breadkit-render 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yamllint +16 -0
- data/CHANGELOG.md +33 -3
- data/Dockerfile +21 -0
- data/Dockerfile.dockerignore +15 -0
- data/README.md +62 -40
- data/docs/REFERENCE.md +115 -0
- data/docs/images/led-netlist.png +0 -0
- data/docs/images/led-schematic.png +0 -0
- data/lib/breadkit/render/apng_encoder.rb +79 -0
- data/lib/breadkit/render/cli.rb +547 -22
- data/lib/breadkit/render/netlist_renderer.rb +138 -0
- data/lib/breadkit/render/orthogonal_router.rb +137 -0
- data/lib/breadkit/render/rasterizer.rb +138 -18
- data/lib/breadkit/render/schematic_renderer.rb +245 -0
- data/lib/breadkit/render/state_selection.rb +23 -0
- data/lib/breadkit/render/svg_renderer.rb +604 -101
- data/lib/breadkit/render/svg_template.rb +84 -0
- data/lib/breadkit/render/theme.rb +46 -0
- data/lib/breadkit/render/version.rb +1 -1
- data/lib/breadkit/render.rb +7 -0
- data/scripts/check-contrast.mjs +28 -0
- data/sig/breadkit/render.rbs +25 -2
- data/site/images/sensor-demo.png +0 -0
- data/site/images/sensor-demo.svg +8 -8
- data/site/index.html +5 -3
- metadata +36 -10
- data/docs/images/sensor-demo.png +0 -0
- data/docs/images/sensor-demo.svg +0 -45
|
@@ -7,8 +7,10 @@ module Breadkit
|
|
|
7
7
|
class SvgRenderer
|
|
8
8
|
PITCH = 10.0
|
|
9
9
|
RAIL_PATTERNS = %w[+--+ +-+- -+-+ -++-].freeze
|
|
10
|
+
LABEL_DENSITIES = %w[full compact none].freeze
|
|
10
11
|
OFFBOARD_PIN_PITCH = 1.0
|
|
11
12
|
PALETTE = %w[#d62728 #1f77b4 #2ca02c #9467bd #ff7f0e #17becf].freeze
|
|
13
|
+
COLORBLIND_PALETTE = %w[#005b96 #a33d00 #006a4e #8a3c68 #176780 #704f9a].freeze
|
|
12
14
|
MODULE_PIN_COLORS = {
|
|
13
15
|
"power" => "#E24B4A", "ground" => "#888780", "clock" => "#EF9F27",
|
|
14
16
|
"data" => "#378ADD", "interrupt" => "#D4537E", "address" => "#888780"
|
|
@@ -37,7 +39,7 @@ module Breadkit
|
|
|
37
39
|
switch_bg: "#ece7dd", switch_border: "#504c45", switch_button: "#c7c0b5", switch_button_border: "#6c655b",
|
|
38
40
|
text: "#25312b", label: "#627168", positive: "#cf5955", negative: "#71847e", negative_wire: "#315ea8", lead: "#68766f",
|
|
39
41
|
component_bg: "#e8eeea", component_border: "#aab7af", module_bg: "#f1efe8",
|
|
40
|
-
module_border: "#888780", muted: "#
|
|
42
|
+
module_border: "#888780", muted: "#66665f", accent: "#1D6B45" },
|
|
41
43
|
"dark" => { board: "#202a27", border: "#52625b", groove: "#141c19", hole: "#718178", used_hole: "#d4e2d9",
|
|
42
44
|
connected_hole: "#9bab9e", resistor_bg: "#6b6248", resistor_border: "#c9ac73",
|
|
43
45
|
diode_bg: "#4b5550", diode_border: "#aab7af", diode_mark: "#dde6df",
|
|
@@ -45,7 +47,7 @@ module Breadkit
|
|
|
45
47
|
switch_bg: "#3d4540", switch_border: "#819188", switch_button: "#58665d", switch_button_border: "#a4b6a9",
|
|
46
48
|
text: "#e8efeb", label: "#c0cdc5", positive: "#ed7168", negative: "#99aaa4", negative_wire: "#76a9f2", lead: "#91a199",
|
|
47
49
|
component_bg: "#2d3935", component_border: "#607168", module_bg: "#343a3d",
|
|
48
|
-
module_border: "#687176", muted: "#
|
|
50
|
+
module_border: "#687176", muted: "#b1bfc2", accent: "#8fbea1" },
|
|
49
51
|
"print" => { board: "#ffffff", border: "#555555", groove: "#eeeeee", hole: "#aaaaaa", used_hole: "#333333",
|
|
50
52
|
connected_hole: "#777777", resistor_bg: "#dddddd", resistor_border: "#555555",
|
|
51
53
|
diode_bg: "#555555", diode_border: "#333333", diode_mark: "#ffffff",
|
|
@@ -53,21 +55,47 @@ module Breadkit
|
|
|
53
55
|
switch_bg: "#eeeeee", switch_border: "#444444", switch_button: "#cccccc", switch_button_border: "#444444",
|
|
54
56
|
text: "#222222", label: "#444444", positive: "#333333", negative: "#777777", negative_wire: "#555555", lead: "#333333",
|
|
55
57
|
component_bg: "#f4f4f4", component_border: "#555555", module_bg: "#f4f4f4",
|
|
56
|
-
module_border: "#666666", muted: "#
|
|
57
|
-
}.
|
|
58
|
+
module_border: "#666666", muted: "#666666", accent: "#333333" }
|
|
59
|
+
}.tap do |palettes|
|
|
60
|
+
palettes["colorblind"] = palettes.fetch("light").merge(
|
|
61
|
+
board: "#f1f5f2", text: "#192923", label: "#43584a", positive: "#a33d00",
|
|
62
|
+
negative: "#53616a", negative_wire: "#005b96", accent: "#006a4e"
|
|
63
|
+
)
|
|
64
|
+
end.freeze
|
|
65
|
+
|
|
66
|
+
def render(circuit, crop: "auto", theme: "light", orientation: "portrait", show_nets: false, legend: false, color_by: "wire", annotations: [], rail_pattern: nil, interactive_layers: true, state: nil, active_layer: nil, focus: nil, highlight_net: nil, label_density: "full", diff_wires: {}, theme_colors: {}, font_css: nil, wire_routing: "declared", wire_style: "raised")
|
|
67
|
+
@multi_board = circuit.respond_to?(:multi_board?) && circuit.multi_board?
|
|
68
|
+
raise ArgumentError, "--rail-pattern is unavailable for multi-board circuits" if @multi_board && rail_pattern
|
|
69
|
+
raise ArgumentError, "unknown label density: #{label_density}" unless LABEL_DENSITIES.include?(label_density)
|
|
70
|
+
raise ArgumentError, "unknown wire routing: #{wire_routing}" unless %w[declared auto].include?(wire_routing)
|
|
71
|
+
raise ArgumentError, "unknown wire style: #{wire_style}" unless %w[raised flat].include?(wire_style)
|
|
58
72
|
|
|
59
|
-
def render(circuit, crop: "auto", theme: "light", orientation: "portrait", show_nets: false, legend: false, color_by: "wire", annotations: [], rail_pattern: nil, interactive_layers: true)
|
|
60
73
|
@circuit, @theme, @orientation, @show_nets, @legend_enabled, @color_by, @annotations = circuit, theme.to_s, orientation.to_s, show_nets, legend, color_by, annotations
|
|
74
|
+
@label_density = label_density
|
|
75
|
+
@wire_routing, @wire_style = wire_routing, wire_style
|
|
76
|
+
@state, @active_layer = state, active_layer&.to_s
|
|
77
|
+
@diff_wires = diff_wires
|
|
78
|
+
@nets = circuit.nets(state)
|
|
79
|
+
@focus_ref = focus&.to_s
|
|
80
|
+
raise ArgumentError, "unknown component: #{@focus_ref}" if @focus_ref && !circuit.components.key?(@focus_ref)
|
|
81
|
+
selected_net = @nets.find { |net| net.name == highlight_net.to_s } if highlight_net
|
|
82
|
+
raise ArgumentError, "unknown net: #{highlight_net}" if highlight_net && !selected_net
|
|
83
|
+
@emphasis_active = !!(@focus_ref || selected_net)
|
|
84
|
+
emphasized_nets = @nets.select { |net| net.members.any? { |member| member.start_with?("#{@focus_ref}.") } } if @focus_ref
|
|
85
|
+
emphasized_nets = emphasized_nets&.select { |net| net == selected_net } if selected_net && @focus_ref
|
|
86
|
+
emphasized_nets ||= [selected_net].compact
|
|
87
|
+
@emphasized_wire_ids = emphasized_nets.flat_map(&:members).to_set
|
|
88
|
+
@emphasized_component_refs = emphasized_nets.flat_map { |net| net.members.filter_map { |member| member.split(".", 2).first if member.include?(".") } }.to_set
|
|
61
89
|
@wire_indexes = circuit.wires.each_with_index.to_h
|
|
62
90
|
@wires_by_id = circuit.wires.to_h { |wire| [wire.id, wire] }
|
|
63
91
|
@rail_definitions = Array(circuit.board.definition.data["rails"]).to_h { |rail| [rail["id"].to_s, rail] }
|
|
64
92
|
@wire_endpoint_refs = circuit.wires.each_with_object({}) { |wire, refs| refs[wire.from] = refs[wire.to] = true }
|
|
65
93
|
@nets_by_member, @nets_by_hole = {}, {}
|
|
66
|
-
|
|
94
|
+
@nets.each do |net|
|
|
67
95
|
net.members.each { |member| @nets_by_member[member] = net }
|
|
68
96
|
net.holes.each { |hole| @nets_by_hole[hole] = net }
|
|
69
97
|
end
|
|
70
|
-
@net_indexes =
|
|
98
|
+
@net_indexes = @nets.each_with_index.to_h
|
|
71
99
|
@board_edge_wires = circuit.wires.select do |wire|
|
|
72
100
|
wire.route == "edge" && !offboard_component(wire.from) && !offboard_component(wire.to)
|
|
73
101
|
end
|
|
@@ -81,23 +109,36 @@ module Breadkit
|
|
|
81
109
|
@offboard_rail_tracks = nil
|
|
82
110
|
@offboard_layouts = {}
|
|
83
111
|
@edge_breakout_offsets = @annotation_states = nil
|
|
112
|
+
@invalid_colors_warned = Set.new
|
|
84
113
|
raise ArgumentError, "orientation must be portrait or landscape" unless %w[portrait landscape].include?(@orientation)
|
|
114
|
+
available_layers = circuit.wires.flat_map { |wire| layer_names(wire.layer) } +
|
|
115
|
+
circuit.components.values.flat_map { |component| layer_names(read(component.attrs, "layer")) }
|
|
116
|
+
raise ArgumentError, "unknown layer: #{@active_layer}" if @active_layer && !available_layers.include?(@active_layer)
|
|
85
117
|
@layers = if interactive_layers
|
|
86
|
-
|
|
87
|
-
circuit.components.values.flat_map { |component| layer_names(read(component.attrs, "layer")) }).uniq
|
|
118
|
+
available_layers.uniq
|
|
88
119
|
else
|
|
89
120
|
[]
|
|
90
121
|
end
|
|
122
|
+
@visible_wires = circuit.wires.select { |wire| visible_in_layer?(wire.layer) }
|
|
123
|
+
@visible_wire_ids = @visible_wires.map(&:id).to_set
|
|
124
|
+
@visible_components = circuit.components.values.select { |component| visible_in_layer?(read(component.attrs, "layer")) }
|
|
125
|
+
@auto_routes = {}
|
|
91
126
|
@layer_controls_height = 0
|
|
92
127
|
@rail_pattern = rail_pattern&.to_s
|
|
93
128
|
configure_rail_pattern
|
|
129
|
+
@auto_router = OrthogonalRouter.new(auto_obstacles) if @wire_routing == "auto"
|
|
94
130
|
|
|
95
|
-
@colors = COLORS.fetch(@theme, COLORS.fetch("light"))
|
|
96
|
-
@
|
|
97
|
-
@used_holes = (
|
|
98
|
-
|
|
131
|
+
@colors = COLORS.fetch(@theme, COLORS.fetch("light")).merge(theme_colors)
|
|
132
|
+
@font_css = font_css
|
|
133
|
+
@used_holes = (@visible_components.flat_map { |component| component.pins.values.map(&:hole_id) } +
|
|
134
|
+
@visible_wires.flat_map { |wire| [wire.from, wire.to].filter_map { |endpoint| circuit.board.hole(endpoint)&.id } } +
|
|
99
135
|
circuit.supplies.flat_map { |supply| [supply.plus, supply.minus] }).compact.to_set
|
|
100
|
-
@
|
|
136
|
+
@conductive_holes = if @active_layer
|
|
137
|
+
@nets.select { |net| visible_net?(net) }.flat_map(&:holes).to_set
|
|
138
|
+
else
|
|
139
|
+
@nets_by_hole.keys.to_set
|
|
140
|
+
end
|
|
141
|
+
@view_box = view_box(@multi_board ? "none" : crop)
|
|
101
142
|
@layer_controls_height = layer_controls_height unless @layers.empty?
|
|
102
143
|
left, top, width, height = @view_box
|
|
103
144
|
extra_height = legend_height
|
|
@@ -109,9 +150,10 @@ module Breadkit
|
|
|
109
150
|
"#{fmt(left)} #{fmt(top)} #{fmt(width)} #{fmt(height + @layer_controls_height + extra_height)}"]
|
|
110
151
|
end
|
|
111
152
|
svg = ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
112
|
-
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"#{fmt(output_width)}\" height=\"#{fmt(output_height)}\" viewBox=\"#{output_view_box}\" font-family=\"Helvetica, Arial, 'Noto Sans JP', sans-serif\" fill=\"#{@colors[:text]}\" role=\"img\"#{@rail_pattern ? " data-rail-pattern=\"#{@rail_pattern}\"" : ""}>",
|
|
153
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"#{fmt(output_width)}\" height=\"#{fmt(output_height)}\" viewBox=\"#{output_view_box}\" font-family=\"#{@font_css ? 'BreadkitEmbedded, ' : ''}Helvetica, Arial, 'Noto Sans JP', sans-serif\" fill=\"#{@colors[:text]}\" role=\"img\"#{@rail_pattern ? " data-rail-pattern=\"#{@rail_pattern}\"" : ""}#{@state ? " data-state=\"#{escape(@state.name)}\"" : ""}>",
|
|
113
154
|
"<title>#{escape(circuit.title || "Breadkit circuit")}</title>",
|
|
114
|
-
"<desc
|
|
155
|
+
"<desc>#{escape((["Breadboard wiring diagram generated by Breadkit.", (@active_layer ? "Showing layer #{@active_layer}; netlist includes all layers." : nil)] + @nets.map { |net| "#{net.name}: #{net.members.join(', ')}" }).compact.join(' '))}</desc>"]
|
|
156
|
+
svg << "<style>#{@font_css}</style>" if @font_css
|
|
115
157
|
transform = if @orientation == "portrait"
|
|
116
158
|
"matrix(0 1 -1 0 #{fmt(top + height)} #{fmt(-left + @layer_controls_height)})"
|
|
117
159
|
elsif @layer_controls_height.positive?
|
|
@@ -124,6 +166,8 @@ module Breadkit
|
|
|
124
166
|
svg << "<g id=\"components\">#{components_svg}</g>"
|
|
125
167
|
svg << "<g id=\"offboard\">#{offboard_svg}</g>"
|
|
126
168
|
svg << "<g id=\"wires\">#{wires_svg}</g>"
|
|
169
|
+
labels = component_labels_overlay_svg
|
|
170
|
+
svg << "<g id=\"component-labels\">#{labels}</g>" unless labels.empty?
|
|
127
171
|
svg << "<g id=\"labels\">#{labels_svg}</g>"
|
|
128
172
|
svg << "<g id=\"nets\">#{nets_svg}</g><g id=\"annotations\">#{annotations_svg}</g>"
|
|
129
173
|
svg << "</g>" if transform
|
|
@@ -168,6 +212,8 @@ module Breadkit
|
|
|
168
212
|
end
|
|
169
213
|
|
|
170
214
|
def rail_polarity(id)
|
|
215
|
+
return @circuit.board.rail_polarity(id) if @multi_board
|
|
216
|
+
|
|
171
217
|
rail_definition(id)["polarity"]&.to_s || (id.end_with?("+", "-") ? id[-1] : nil)
|
|
172
218
|
end
|
|
173
219
|
|
|
@@ -193,6 +239,8 @@ module Breadkit
|
|
|
193
239
|
end
|
|
194
240
|
|
|
195
241
|
def board_svg
|
|
242
|
+
return named_boards_svg if @multi_board
|
|
243
|
+
|
|
196
244
|
x, y, width, height = board_bounds
|
|
197
245
|
out = [rect(x, y, width, height, rx: 8, fill: @colors[:board], stroke: @colors[:border], stroke_width: 1)]
|
|
198
246
|
ravine = Array(@circuit.board.definition.data.dig("terminal", "ravine_between"))
|
|
@@ -207,35 +255,82 @@ module Breadkit
|
|
|
207
255
|
next unless id.start_with?("rail:")
|
|
208
256
|
holes = ids.map { |hole_id| @circuit.board.hole(hole_id) }
|
|
209
257
|
next if holes.empty?
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
258
|
+
out << rail_strip_svg(holes)
|
|
259
|
+
end
|
|
260
|
+
out.join
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def named_boards_svg
|
|
264
|
+
board = @circuit.board
|
|
265
|
+
out = board.boards.map do |name, definition|
|
|
266
|
+
holes = board.holes.values.select { |hole| hole.id.start_with?("#{name}.") }
|
|
267
|
+
xs, ys = holes.map { |hole| px(hole.x) }, holes.map { |hole| py(hole.y) }
|
|
268
|
+
x, y = xs.min - PITCH * 1.5, ys.min - PITCH * 1.5
|
|
269
|
+
width, height = xs.max - xs.min + PITCH * 3, ys.max - ys.min + PITCH * 3
|
|
270
|
+
plate = [rect(x, y, width, height, rx: 8, fill: @colors[:board], stroke: @colors[:border], stroke_width: 1)]
|
|
271
|
+
ravine = Array(definition.definition.data.dig("terminal", "ravine_between"))
|
|
272
|
+
rows = ravine.map { |row| holes.find { |hole| hole.row == row.to_s } }
|
|
273
|
+
if rows.length == 2 && rows.all?
|
|
274
|
+
groove_y = rows.sum { |hole| py(hole.y) } / rows.length
|
|
275
|
+
plate << rect(x + PITCH / 2, groove_y - PITCH, width - PITCH, PITCH * 2,
|
|
276
|
+
rx: 2, fill: @colors[:groove])
|
|
277
|
+
end
|
|
278
|
+
plate << text(x + width - 17, y + 9, name, "font-size" => 8, "font-weight" => 700,
|
|
279
|
+
"fill" => @colors[:text], "text-anchor" => "middle")
|
|
280
|
+
"<g data-board=\"#{escape(name)}\">#{plate.join}</g>"
|
|
281
|
+
end
|
|
282
|
+
board.strips.each do |id, ids|
|
|
283
|
+
next unless id.include?("rail:")
|
|
284
|
+
|
|
285
|
+
holes = ids.map { |hole_id| board.hole(hole_id) }
|
|
286
|
+
next if holes.empty?
|
|
287
|
+
out << rail_strip_svg(holes)
|
|
216
288
|
end
|
|
217
289
|
out.join
|
|
218
290
|
end
|
|
219
291
|
|
|
292
|
+
def rail_strip_svg(holes)
|
|
293
|
+
rail_id = holes.first.rail
|
|
294
|
+
color = { "+" => @colors[:positive], "-" => @colors[:negative] }.fetch(rail_polarity(rail_id), @colors[:label])
|
|
295
|
+
positions = holes.map { |hole| display_hole(hole) }
|
|
296
|
+
left, right = positions.map { |hole| px(hole.x) }.minmax
|
|
297
|
+
top, bottom = positions.map { |hole| py(hole.y) }.minmax
|
|
298
|
+
if bottom - top > right - left
|
|
299
|
+
rect((left + right) / 2 - 3, top - 3, 6, bottom - top + 6,
|
|
300
|
+
rx: 3, fill: color, opacity: 0.14, data_rail: rail_id)
|
|
301
|
+
else
|
|
302
|
+
rect(left - 3, (top + bottom) / 2 - 3, right - left + 6, 6,
|
|
303
|
+
rx: 3, fill: color, opacity: 0.14, data_rail: rail_id)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
220
307
|
def holes_svg
|
|
221
|
-
|
|
308
|
+
output = ["<defs><circle id=\"h\" r=\"1.3\"/><circle id=\"u\" r=\"1.8\"/></defs>",
|
|
309
|
+
"<style>#holes use{fill:#{@colors[:hole]};opacity:.75}#holes use.c{fill:#{@colors[:connected_hole]}}#holes use.o{fill:#{@colors[:used_hole]};opacity:1}</style>"]
|
|
310
|
+
output.concat(@circuit.board.holes.values.map do |hole|
|
|
222
311
|
used = @used_holes.include?(hole.id)
|
|
223
312
|
connected = @conductive_holes.include?(hole.id)
|
|
224
313
|
display = display_hole(hole)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
attrs
|
|
229
|
-
|
|
230
|
-
|
|
314
|
+
dot = used ? "u" : "h"
|
|
315
|
+
class_name = used ? " class=\"o\" data-occupied=\"true\"" : (connected ? " class=\"c\"" : "")
|
|
316
|
+
class_name += " data-connected=\"true\"" if connected
|
|
317
|
+
attrs = "href=\"##{dot}\" x=\"#{coord(px(display.x))}\" y=\"#{coord(py(display.y))}\" data-hole=\"#{escape(hole.id)}\"#{class_name}"
|
|
318
|
+
attrs += " data-net=\"#{escape(@nets_by_hole[hole.id].name)}\"" if @nets_by_hole[hole.id]
|
|
319
|
+
title = [hole.id, @nets_by_hole[hole.id]&.name].compact.join(" / ")
|
|
320
|
+
"<use #{attrs}><title>#{escape(title)}</title></use>"
|
|
321
|
+
end)
|
|
322
|
+
output.join
|
|
231
323
|
end
|
|
232
324
|
|
|
233
325
|
def labels_svg
|
|
326
|
+
return named_board_labels_svg if @multi_board
|
|
327
|
+
|
|
234
328
|
out = []
|
|
235
329
|
number_y = @orientation == "portrait" ? py(-1) : py(@circuit.board.height)
|
|
236
330
|
@circuit.board.definition.data.dig("terminal", "rows").each do |row|
|
|
237
331
|
y = @circuit.board.holes.values.find { |hole| hole.row == row }&.y
|
|
238
|
-
|
|
332
|
+
row_x = @orientation == "landscape" ? board_bounds.then { |left, _top, width, _height| left + width - 6 } : -7
|
|
333
|
+
out << text(row_x, py(y), row, "font-size" => 8, "font-weight" => 500, "fill" => @colors[:label], "text-anchor" => "middle") if y
|
|
239
334
|
end
|
|
240
335
|
(1..@circuit.board.width).each do |col|
|
|
241
336
|
next unless col == 1 || (col % 5).zero?
|
|
@@ -250,6 +345,30 @@ module Breadkit
|
|
|
250
345
|
out.join
|
|
251
346
|
end
|
|
252
347
|
|
|
348
|
+
def named_board_labels_svg
|
|
349
|
+
out = []
|
|
350
|
+
@circuit.board.boards.each do |name, definition|
|
|
351
|
+
holes = @circuit.board.holes.values.select { |hole| hole.id.start_with?("#{name}.") }
|
|
352
|
+
terminal_holes = holes.select { |hole| hole.kind == :terminal }
|
|
353
|
+
left_x = terminal_holes.map(&:x).min
|
|
354
|
+
Array(definition.definition.data.dig("terminal", "rows")).each do |row|
|
|
355
|
+
hole = terminal_holes.find { |item| item.row == row.to_s }
|
|
356
|
+
out << text(px(left_x) - 7, py(hole.y), row, "font-size" => 8, "font-weight" => 500,
|
|
357
|
+
"fill" => @colors[:label], "text-anchor" => "middle") if hole
|
|
358
|
+
end
|
|
359
|
+
number_y = @orientation == "portrait" ? py(-1) : py(definition.height)
|
|
360
|
+
(1..definition.width).each do |col|
|
|
361
|
+
next unless col == 1 || (col % 5).zero?
|
|
362
|
+
|
|
363
|
+
number_x = px(left_x + col - 1) + (@orientation == "portrait" ? 5 : 0)
|
|
364
|
+
out << text(number_x + (@orientation == "portrait" ? 3 : 0), number_y, col,
|
|
365
|
+
"font-size" => @orientation == "portrait" ? 6 : 7, "font-weight" => 500,
|
|
366
|
+
"fill" => @colors[:text], "text-anchor" => "middle")
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
out.join
|
|
370
|
+
end
|
|
371
|
+
|
|
253
372
|
def components_svg
|
|
254
373
|
@circuit.components.values.map do |component|
|
|
255
374
|
pins = component.pins.values.filter_map { |pin| [pin, @circuit.board.hole(pin.hole_id)] if pin.hole_id }
|
|
@@ -257,7 +376,20 @@ module Breadkit
|
|
|
257
376
|
xs, ys = pins.map { |_pin, hole| px(display_hole(hole).x) }, pins.map { |_pin, hole| py(display_hole(hole).y) }
|
|
258
377
|
center_x, center_y = (xs.min + xs.max) / 2.0, (ys.min + ys.max) / 2.0
|
|
259
378
|
shape = component.part.data.dig("render", "shape")
|
|
260
|
-
|
|
379
|
+
shape = "to92" if shape == "generic" && %w[transistor 2n3904 2n3906 bc547 bc557].include?(component.part.id)
|
|
380
|
+
shape = "potentiometer" if shape == "generic" && component.part.id == "pot"
|
|
381
|
+
template = component.part.data.dig("render", "svg")
|
|
382
|
+
body = if rail_module?(component)
|
|
383
|
+
rail_module_svg(component, pins)
|
|
384
|
+
elsif template
|
|
385
|
+
values = { "ref" => component.ref, "value" => component.value,
|
|
386
|
+
"fill" => component.part.data.dig("render", "fill") || @colors[:component_bg],
|
|
387
|
+
"stroke" => component.part.data.dig("render", "stroke") || @colors[:component_border],
|
|
388
|
+
"text_color" => component.part.data.dig("render", "text_color") || @colors[:text] }
|
|
389
|
+
lead_lines(pins, center_x, center_y) +
|
|
390
|
+
%(<g data-part-template="#{escape(component.part.id)}" transform="translate(#{fmt(center_x)} #{fmt(center_y)})">#{SvgTemplate.new(template).render(values)}</g>)
|
|
391
|
+
else
|
|
392
|
+
case shape
|
|
261
393
|
when "resistor" then resistor_svg(component, pins, center_x, center_y)
|
|
262
394
|
when "led_5mm" then led_svg(component, pins, center_x, center_y)
|
|
263
395
|
when "diode" then diode_svg(component, pins, center_x, center_y)
|
|
@@ -265,9 +397,16 @@ module Breadkit
|
|
|
265
397
|
when "dip" then dip_svg(component, pins)
|
|
266
398
|
when "module" then module_svg(component, pins)
|
|
267
399
|
when "tact_switch" then switch_svg(component, pins, center_x, center_y)
|
|
400
|
+
when "seven_segment" then seven_segment_svg(component, pins, center_x, center_y)
|
|
401
|
+
when "rgb_led_5mm" then rgb_led_svg(component, pins, center_x, center_y)
|
|
402
|
+
when "to92" then to92_svg(component, pins, center_x, center_y)
|
|
403
|
+
when "potentiometer" then potentiometer_svg(component, pins, center_x, center_y)
|
|
268
404
|
else generic_svg(component, pins, center_x, center_y)
|
|
405
|
+
end
|
|
269
406
|
end
|
|
270
|
-
|
|
407
|
+
opacity = @emphasis_active && !emphasized_component?(component.ref) ? " opacity=\"0.18\"" : ""
|
|
408
|
+
switch = Array(component.part.data["switch"]).empty? ? "" : " data-switch=\"#{escape(component.ref)}\""
|
|
409
|
+
body = "<g data-ref=\"#{escape(component.ref)}\"#{switch}#{opacity}>#{body}</g>"
|
|
271
410
|
layer_group(body, read(component.attrs, "layer"))
|
|
272
411
|
end.compact.join
|
|
273
412
|
end
|
|
@@ -277,27 +416,95 @@ module Breadkit
|
|
|
277
416
|
out << rect(x - 12, y - 4, 24, 8, rx: 3, fill: @colors[:resistor_bg], stroke: @colors[:resistor_border], data_ref: component.ref)
|
|
278
417
|
bands = resistor_bands(component.value, bands: component.attrs[:bands] || 4)
|
|
279
418
|
bands.each_with_index { |color, index| out << rect(x - (bands.length == 5 ? 9 : 7) + index * 4, y - 4, 1.7, 8, fill: color) }
|
|
280
|
-
out << text(x, y - 7, "#{component.ref} #{component.value}", "font-size" => 5, "font-weight" => 500, "text-anchor" => "middle")
|
|
281
419
|
out.join
|
|
282
420
|
end
|
|
283
421
|
|
|
422
|
+
def component_labels_overlay_svg
|
|
423
|
+
return "" if @label_density == "none"
|
|
424
|
+
|
|
425
|
+
@label_wire_bounds = @visible_wires.filter_map do |wire|
|
|
426
|
+
from, to = endpoint_point(wire.from), endpoint_point(wire.to)
|
|
427
|
+
next unless from && to
|
|
428
|
+
|
|
429
|
+
wire_route_points(wire, from, to).each_cons(2).map do |first, last|
|
|
430
|
+
[*([px(first[0]), px(last[0])].minmax), *([py(first[1]), py(last[1])].minmax)]
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
@visible_components.filter_map do |component|
|
|
434
|
+
holes = component.pins.values.filter_map { |pin| @circuit.board.hole(pin.hole_id) }
|
|
435
|
+
next if holes.empty?
|
|
436
|
+
|
|
437
|
+
xs, ys = holes.map { |hole| px(display_hole(hole).x) }, holes.map { |hole| py(display_hole(hole).y) }
|
|
438
|
+
x, y = xs.minmax.sum / 2.0, ys.minmax.sum / 2.0
|
|
439
|
+
shape = component.part.data.dig("render", "shape")
|
|
440
|
+
label, baseline, font_size, background, foreground = case shape
|
|
441
|
+
when "resistor" then [value_label(component, shape), y - 7, 5, @colors[:board], @colors[:text]]
|
|
442
|
+
when "led_5mm" then [component.ref, y - 8, 5, @colors[:board], @colors[:text]]
|
|
443
|
+
when "diode" then [component.ref, y - 5, 4.5, @colors[:board], @colors[:text]]
|
|
444
|
+
when "capacitor", "electrolytic" then [value_label(component, shape), y - 6, 4.5, @colors[:board], @colors[:text]]
|
|
445
|
+
when "dip" then [component.part.data.dig("render", "label") || component.ref, y + 1.5, 4.5, @colors[:dip_bg], @colors[:dip_text]]
|
|
446
|
+
else next
|
|
447
|
+
end
|
|
448
|
+
width = [label.to_s.length * font_size * 0.62 + 6, 13].max
|
|
449
|
+
baseline = clear_label_baseline(x, baseline, width, font_size)
|
|
450
|
+
background_options = { rx: 2, fill: background }
|
|
451
|
+
background_options[:transform] = "rotate(-90 #{fmt(x)} #{fmt(baseline)})" if @rotated_scene
|
|
452
|
+
opacity = @emphasis_active && !emphasized_component?(component.ref) ? " opacity=\"0.18\"" : ""
|
|
453
|
+
body = rect(x - width / 2, baseline - font_size - 2, width, font_size + 4, **background_options)
|
|
454
|
+
body += text(x, baseline, label, "font-size" => font_size, "font-weight" => 500,
|
|
455
|
+
"fill" => foreground, "text-anchor" => "middle", "paint-order" => "stroke",
|
|
456
|
+
"stroke" => background, "stroke-width" => 1.5, "stroke-linejoin" => "round")
|
|
457
|
+
layer_group("<g data-ref=\"#{escape(component.ref)}\"#{opacity}>#{body}</g>", read(component.attrs, "layer"))
|
|
458
|
+
end.join
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def value_label(component, shape)
|
|
462
|
+
return component.ref if @label_density == "compact" || !component.value
|
|
463
|
+
|
|
464
|
+
"#{component.ref} #{Breadkit::Value.new(component.value, category: shape)}"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def clear_label_baseline(x, baseline, width, font_size)
|
|
468
|
+
near = width / 2 + 5
|
|
469
|
+
far = width / 2 + 12
|
|
470
|
+
[baseline, baseline - near, baseline + near, baseline - far, baseline + far].min_by do |candidate|
|
|
471
|
+
bounds = label_bounds(x, candidate, width, font_size)
|
|
472
|
+
view_left, view_top, view_width, view_height = @view_box
|
|
473
|
+
clipped = bounds[0] < view_left || bounds[1] < view_top ||
|
|
474
|
+
bounds[2] > view_left + view_width || bounds[3] > view_top + view_height
|
|
475
|
+
[clipped ? 1 : 0, label_wire_overlaps(bounds), (candidate - baseline).abs]
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def label_bounds(x, baseline, width, font_size)
|
|
480
|
+
return [x - font_size - 2, baseline - width / 2, x + 2, baseline + width / 2] if @rotated_scene
|
|
481
|
+
|
|
482
|
+
[x - width / 2, baseline - font_size - 2, x + width / 2, baseline + 2]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def label_wire_overlaps(bounds)
|
|
486
|
+
left, top, right, bottom = bounds
|
|
487
|
+
@label_wire_bounds.count do |segments|
|
|
488
|
+
segments.any? do |x1, x2, y1, y2|
|
|
489
|
+
x1 <= right + 1 && x2 >= left - 1 && y1 <= bottom + 1 && y2 >= top - 1
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
284
494
|
def led_svg(component, pins, x, y)
|
|
285
495
|
out = [lead_lines(pins, x, y)]
|
|
286
496
|
color = component.attrs[:color].to_s.downcase
|
|
287
497
|
color = "red" if color.empty?
|
|
288
|
-
|
|
289
|
-
raise Error, "invalid LED color: #{color}" unless valid_color
|
|
498
|
+
color = render_color(color, "LED #{component.ref}") { "red" }
|
|
290
499
|
color = { "red" => "#df5550", "green" => "#5fbf69", "blue" => "#6a9df0", "yellow" => "#f0d958" }.fetch(color, color)
|
|
291
500
|
out << circle(x, y, 5.5, fill: color, stroke: @colors[:component_border], data_ref: component.ref)
|
|
292
501
|
out << polarity_marker(component, pins, x, y, 5.0, @colors[:component_border])
|
|
293
|
-
out << text(x, y - 8, component.ref, "font-size" => 5, "font-weight" => 500, "text-anchor" => "middle")
|
|
294
502
|
out.join
|
|
295
503
|
end
|
|
296
504
|
|
|
297
505
|
def diode_svg(component, pins, x, y)
|
|
298
506
|
[lead_lines(pins, x, y), rect(x - 7, y - 3, 14, 6, rx: 1, fill: @colors[:diode_bg], stroke: @colors[:diode_border], data_ref: component.ref),
|
|
299
|
-
polarity_marker(component, pins, x, y, 3.0, @colors[:diode_mark])
|
|
300
|
-
text(x, y - 5, component.ref, "font-size" => 4.5, "font-weight" => 500, "text-anchor" => "middle")].join
|
|
507
|
+
polarity_marker(component, pins, x, y, 3.0, @colors[:diode_mark])].join
|
|
301
508
|
end
|
|
302
509
|
|
|
303
510
|
def capacitor_svg(component, pins, x, y, electrolytic:)
|
|
@@ -310,7 +517,6 @@ module Breadkit
|
|
|
310
517
|
offset_y = offset_x.zero? ? (positive && positive[1] > y ? 5 : -5) : 0
|
|
311
518
|
out << text(x + offset_x, y + offset_y + 1.5, "+", "font-size" => 4, "text-anchor" => "middle")
|
|
312
519
|
end
|
|
313
|
-
out << text(x, y - 6, component.ref, "font-size" => 4.5, "font-weight" => 500, "text-anchor" => "middle")
|
|
314
520
|
out.join
|
|
315
521
|
end
|
|
316
522
|
|
|
@@ -322,11 +528,11 @@ module Breadkit
|
|
|
322
528
|
notch_x = first_pin && px(first_pin.x) > xs.sum / xs.length ? x2 - 2 : x1 + 2
|
|
323
529
|
out << tag("path", d: "M #{fmt(notch_x)} #{fmt(middle - 2)} Q #{fmt(notch_x + (notch_x == x1 + 2 ? 2 : -2))} #{fmt(middle)} #{fmt(notch_x)} #{fmt(middle + 2)}",
|
|
324
530
|
fill: "none", stroke: @colors[:dip_notch], stroke_width: 0.8)
|
|
325
|
-
out << text((x1 + x2) / 2, middle + 1.5, component.part.data.dig("render", "label") || component.ref,
|
|
326
|
-
"font-size" => 4.5, "fill" => @colors[:dip_text], "text-anchor" => "middle")
|
|
327
531
|
pins.each do |pin, hole|
|
|
328
532
|
display = display_hole(hole)
|
|
329
|
-
|
|
533
|
+
number = text(px(display.x), py(display.y) + (display.y > 4 ? 4 : -2), pin.number,
|
|
534
|
+
"font-size" => 3.5, "text-anchor" => "middle")
|
|
535
|
+
out << %(<g data-dip-pin="#{escape(component.ref)}.#{escape(pin.name)}"><title>Pin #{escape(pin.number)}: #{escape(pin.name)}</title>#{number}</g>)
|
|
330
536
|
end
|
|
331
537
|
out.join
|
|
332
538
|
end
|
|
@@ -340,6 +546,9 @@ module Breadkit
|
|
|
340
546
|
stroke: render["stroke"] || @colors[:module_border], stroke_width: 1,
|
|
341
547
|
data_ref: component.ref)]
|
|
342
548
|
pin_definitions = component.part.data["pins"].to_h { |definition| [definition["num"].to_s, definition] }
|
|
549
|
+
pad_points = pins.map { |_pin, hole| display_hole(hole).then { |display| [px(display.x), py(display.y)] } }
|
|
550
|
+
rotated_footprint = [90, 270].include?(component.attrs[:rotate])
|
|
551
|
+
portrait_labels, landscape_labels = [], []
|
|
343
552
|
pins.each do |pin, hole|
|
|
344
553
|
display = display_hole(hole)
|
|
345
554
|
pin_x, pin_y = px(display.x), py(display.y)
|
|
@@ -352,11 +561,42 @@ module Breadkit
|
|
|
352
561
|
label = definition && definition["label"]
|
|
353
562
|
next unless label
|
|
354
563
|
label = pin.name if label == true
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
564
|
+
length = label.to_s.length * 5.8 * 0.6
|
|
565
|
+
label_transform = nil
|
|
566
|
+
if @orientation == "landscape" && width > height * 1.5 && pins.length >= 8
|
|
567
|
+
inward = center_y > pin_y
|
|
568
|
+
label_x, label_y = pin_x, pin_y + (inward ? 6 : -6)
|
|
569
|
+
label_anchor = inward ? "start" : "end"
|
|
570
|
+
label_transform = "rotate(90 #{fmt(label_x)} #{fmt(label_y)})"
|
|
571
|
+
elsif @orientation == "landscape" && rotated_footprint
|
|
572
|
+
label_x = pin_x
|
|
573
|
+
label_y = [pin_y - 9, pin_y + 9, pin_y - 18, pin_y + 18].find do |candidate|
|
|
574
|
+
candidate.between?(y + 4, y + height - 4) &&
|
|
575
|
+
landscape_labels.none? { |other_x, other_y, other_length| (label_x - other_x).abs < (length + other_length) / 2 &&
|
|
576
|
+
(candidate - other_y).abs < 5.8 } &&
|
|
577
|
+
pad_points.none? { |pad_x, pad_y| (label_x - pad_x).abs < length / 2 + 3.3 && (candidate - pad_y).abs < 6.2 }
|
|
578
|
+
end || pin_y
|
|
579
|
+
landscape_labels << [label_x, label_y, length]
|
|
580
|
+
label_anchor = "middle"
|
|
581
|
+
elsif @orientation == "portrait" && rotated_footprint
|
|
582
|
+
label_y = pin_y - length / 2 - 9
|
|
583
|
+
label_x = [pin_x, pin_x - 9, pin_x + 9, pin_x - 18, pin_x + 18].find do |candidate|
|
|
584
|
+
candidate.between?(x + 3, x + width - 3) &&
|
|
585
|
+
portrait_labels.none? { |other_x, other_y, other_length| (candidate - other_x).abs < 5.8 &&
|
|
586
|
+
(label_y - other_y).abs < (length + other_length) / 2 } &&
|
|
587
|
+
pad_points.none? { |pad_x, pad_y| (candidate - pad_x).abs < 6.2 && (label_y - pad_y).abs < length / 2 + 3.3 }
|
|
588
|
+
end || pin_x
|
|
589
|
+
portrait_labels << [label_x, label_y, length]
|
|
590
|
+
label_anchor = "middle"
|
|
591
|
+
else
|
|
592
|
+
label_gap = @orientation == "portrait" ? 10 : 3.8
|
|
593
|
+
label_x, label_y = pin_x, pin_y + (center_y > pin_y ? label_gap : -label_gap)
|
|
594
|
+
label_anchor = @orientation == "portrait" ? (center_y > pin_y ? "end" : "start") : "middle"
|
|
595
|
+
end
|
|
596
|
+
attrs = { "font-size" => 5.8, "font-weight" => 500, "fill" => render["text_color"] || @colors[:text],
|
|
597
|
+
"text-anchor" => label_anchor }
|
|
598
|
+
attrs["transform"] = label_transform if label_transform
|
|
599
|
+
out << text(label_x, label_y, label, attrs)
|
|
360
600
|
end
|
|
361
601
|
label = render["label"] || component.ref
|
|
362
602
|
font_size = [[ [width, height].min / 6, 6].max, 10].min
|
|
@@ -377,6 +617,8 @@ module Breadkit
|
|
|
377
617
|
end
|
|
378
618
|
|
|
379
619
|
def module_bounds(component, pins)
|
|
620
|
+
return component.body_bounds(@circuit.board) if component.respond_to?(:body_bounds)
|
|
621
|
+
|
|
380
622
|
xs, ys = pins.map { |_pin, hole| hole.x }, pins.map { |_pin, hole| hole.y }
|
|
381
623
|
render = component.part.data.fetch("render")
|
|
382
624
|
width_mm, height_mm = render.fetch("size_mm").map { |value| Float(value) }
|
|
@@ -388,10 +630,64 @@ module Breadkit
|
|
|
388
630
|
[center_x - width / 2, center_y - height / 2, width, height]
|
|
389
631
|
end
|
|
390
632
|
|
|
633
|
+
def rail_module?(component)
|
|
634
|
+
definitions = component.part.data["pins"] || []
|
|
635
|
+
!definitions.empty? && definitions.all? { |pin| pin["mount"] == "rail" }
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def rail_module_bounds(pins)
|
|
639
|
+
xs = pins.map { |_pin, hole| hole.x }
|
|
640
|
+
ys = pins.map { |_pin, hole| hole.y }
|
|
641
|
+
board_left, board_right = @board_extents.fetch(:x)
|
|
642
|
+
width, height = 19.0, 11.0
|
|
643
|
+
x = xs.sum / xs.length.to_f <= (board_left + board_right) / 2.0 ? board_left - 21.5 : board_right + 2.5
|
|
644
|
+
[x, (ys.min + ys.max) / 2.0 - height / 2, width, height]
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def rail_pin_label(pin, index)
|
|
648
|
+
{ "LEFT_POS" => "L+", "LEFT_GND" => "L−", "RIGHT_POS" => "R+", "RIGHT_GND" => "R−" }.fetch(pin.name) { ("A".ord + index).chr }
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def rail_module_svg(component, pins)
|
|
652
|
+
x, y, width, height = rail_module_bounds(pins)
|
|
653
|
+
title = component.part.data.dig("render", "label") || component.part.id
|
|
654
|
+
out = ["<title>#{escape(title)} · logical pin map; verify physical fit</title>",
|
|
655
|
+
rect(px(x), py(y + height), px(width), px(height), rx: 3,
|
|
656
|
+
fill: @colors[:module_bg], stroke: @colors[:module_border], stroke_width: 1,
|
|
657
|
+
data_ref: component.ref)]
|
|
658
|
+
out << text(px(x + 0.8), py(y + height - 1.4), component.ref, "font-size" => 13,
|
|
659
|
+
"font-weight" => 700, "fill" => @colors[:text])
|
|
660
|
+
out << text(px(x + 0.8), py(y + height - 2.5), "Logical pins · verify fit", "font-size" => 10,
|
|
661
|
+
"fill" => @colors[:label])
|
|
662
|
+
master = read(component.attrs, "master")
|
|
663
|
+
left = read(component.attrs, "left")
|
|
664
|
+
right = read(component.attrs, "right")
|
|
665
|
+
if master || left || right
|
|
666
|
+
mode = ->(value) { { "v3_3" => "3.3 V", "v5" => "5 V", "off" => "OFF" }.fetch(value.to_s, value.to_s) }
|
|
667
|
+
out << text(px(x + 0.8), py(y + height - 3.5), "Master #{master.to_s.upcase} · Left #{mode.call(left)}", "font-size" => 9.2,
|
|
668
|
+
"fill" => @colors[:text])
|
|
669
|
+
out << text(px(x + 0.8), py(y + height - 4.4), "Right #{mode.call(right)}", "font-size" => 9.2,
|
|
670
|
+
"fill" => @colors[:text])
|
|
671
|
+
end
|
|
672
|
+
pins.each_with_index do |(pin, hole), index|
|
|
673
|
+
display = display_hole(hole)
|
|
674
|
+
color = MODULE_PIN_COLORS.fetch(pin.role.to_s, @colors[:lead])
|
|
675
|
+
short = rail_pin_label(pin, index)
|
|
676
|
+
out << circle(px(display.x), py(display.y), 6, fill: color, stroke: @colors[:module_border],
|
|
677
|
+
stroke_width: 0.8, data_pin: "#{component.ref}.#{pin.name}")
|
|
678
|
+
out << text(px(display.x), py(display.y) + 2.2, short, "font-size" => 6.5,
|
|
679
|
+
"font-weight" => 700, "fill" => "#ffffff", "text-anchor" => "middle")
|
|
680
|
+
out << text(px(x + 0.8), py(y + height - 6.0 - index),
|
|
681
|
+
"#{short} #{pin.name} #{hole.id}", "font-size" => 10, "fill" => @colors[:text])
|
|
682
|
+
end
|
|
683
|
+
%(<g data-rail-module="logical">#{out.join}</g>)
|
|
684
|
+
end
|
|
685
|
+
|
|
391
686
|
def switch_svg(component, pins, x, y)
|
|
392
687
|
out = [lead_lines(pins, x, y)]
|
|
393
688
|
out << rect(x - 12, y - 10, 24, 20, rx: 2, fill: @colors[:switch_bg], stroke: @colors[:switch_border], data_ref: component.ref)
|
|
394
|
-
|
|
689
|
+
closed = @state&.closed_switches&.any? { |item, _pair| item.equal?(component) }
|
|
690
|
+
out << circle(x, y, 5, fill: closed ? @colors[:accent] : @colors[:switch_button], stroke: @colors[:switch_button_border])
|
|
395
691
|
out << text(x, y - 13, component.ref, "font-size" => 5, "font-weight" => 500, "text-anchor" => "middle")
|
|
396
692
|
out.join
|
|
397
693
|
end
|
|
@@ -403,6 +699,53 @@ module Breadkit
|
|
|
403
699
|
out.join
|
|
404
700
|
end
|
|
405
701
|
|
|
702
|
+
def to92_svg(component, pins, x, y)
|
|
703
|
+
out = [lead_lines(pins, x, y)]
|
|
704
|
+
path = "M #{fmt(x - 8)} #{fmt(y - 4)} H #{fmt(x + 8)} V #{fmt(y)} A 8 8 0 0 1 #{fmt(x - 8)} #{fmt(y)} Z"
|
|
705
|
+
out << tag("path", d: path, fill: @colors[:dip_bg], stroke: @colors[:dip_border], stroke_width: 1,
|
|
706
|
+
data_shape: "to92")
|
|
707
|
+
out << text(x, y + 1.5, component.ref, "font-size" => 4.5, "fill" => @colors[:dip_text], "text-anchor" => "middle")
|
|
708
|
+
out.join
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def potentiometer_svg(component, pins, x, y)
|
|
712
|
+
out = [lead_lines(pins, x, y)]
|
|
713
|
+
out << rect(x - 12, y - 8, 24, 16, rx: 2, fill: @colors[:component_bg], stroke: @colors[:component_border])
|
|
714
|
+
out << circle(x, y, 6, fill: @colors[:switch_button], stroke: @colors[:switch_button_border],
|
|
715
|
+
data_shape: "potentiometer")
|
|
716
|
+
out << line(x - 3, y + 3, x + 3, y - 3, @colors[:switch_button_border], 1.2)
|
|
717
|
+
out << text(x, y - 11, component.ref, "font-size" => 4.5, "text-anchor" => "middle")
|
|
718
|
+
out.join
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def seven_segment_svg(component, pins, x, y)
|
|
722
|
+
out = [lead_lines(pins, x, y)]
|
|
723
|
+
out << rect(x - 13, y - 23, 26, 46, rx: 2, fill: @colors[:component_bg],
|
|
724
|
+
stroke: @colors[:component_border], data_ref: component.ref)
|
|
725
|
+
out << rect(x - 9, y - 19, 18, 38, rx: 1, fill: "#201c20")
|
|
726
|
+
segments = {
|
|
727
|
+
"A" => [x - 6, y - 16, 12, 2], "G" => [x - 6, y - 1, 12, 2], "D" => [x - 6, y + 14, 12, 2],
|
|
728
|
+
"F" => [x - 8, y - 14, 2, 12], "B" => [x + 6, y - 14, 2, 12],
|
|
729
|
+
"E" => [x - 8, y + 2, 2, 12], "C" => [x + 6, y + 2, 2, 12]
|
|
730
|
+
}
|
|
731
|
+
segments.each do |name, (left, top, width, height)|
|
|
732
|
+
out << rect(left, top, width, height, rx: 0.5, fill: "#d96960", opacity: 0.45, data_segment: name)
|
|
733
|
+
end
|
|
734
|
+
out << circle(x + 6, y + 16, 1, fill: "#d96960", opacity: 0.45, data_segment: "DP")
|
|
735
|
+
out << text(x, y - 26, component.ref, "font-size" => 5, "text-anchor" => "middle")
|
|
736
|
+
out.join
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def rgb_led_svg(component, pins, x, y)
|
|
740
|
+
out = [lead_lines(pins, x, y)]
|
|
741
|
+
out << circle(x, y, 7, fill: @colors[:component_bg], stroke: @colors[:component_border], data_ref: component.ref)
|
|
742
|
+
{ "R" => [-3.3, "#e35d59"], "G" => [0, "#66c786"], "B" => [3.3, "#6a9df0"] }.each do |channel, (offset, color)|
|
|
743
|
+
out << circle(x + offset, y, 2.4, fill: color, opacity: 0.75, data_channel: channel)
|
|
744
|
+
end
|
|
745
|
+
out << text(x, y - 10, component.ref, "font-size" => 5, "text-anchor" => "middle")
|
|
746
|
+
out.join
|
|
747
|
+
end
|
|
748
|
+
|
|
406
749
|
def offboard_svg
|
|
407
750
|
@circuit.components.values.filter_map do |component|
|
|
408
751
|
next unless component.part.placement == "offboard"
|
|
@@ -428,7 +771,8 @@ module Breadkit
|
|
|
428
771
|
pin_x, pin_y = offboard_pin_point(component, index)
|
|
429
772
|
pin_color = MODULE_PIN_COLORS.fetch(pin.role.to_s, @colors[:lead])
|
|
430
773
|
out << circle(px(pin_x), py(pin_y), 1.7, fill: pin_color, stroke: @colors[:module_bg],
|
|
431
|
-
stroke_width: 0.6, data_pin: "#{component.ref}.#{pin.name}", data_pin_type: pin.role
|
|
774
|
+
stroke_width: 0.6, data_pin: "#{component.ref}.#{pin.name}", data_pin_type: pin.role,
|
|
775
|
+
data_net: @nets_by_member["#{component.ref}.#{pin.name}"]&.name)
|
|
432
776
|
references = ["#{component.ref}.#{pin.name}", "#{component.ref}.#{pin.number}"]
|
|
433
777
|
connected = references.any? { |reference| @wire_endpoint_refs.key?(reference) }
|
|
434
778
|
label_side = component.attrs[:side].to_s == "left"
|
|
@@ -446,7 +790,8 @@ module Breadkit
|
|
|
446
790
|
out << text(label_x, label_y, pin.name, "font-size" => 5.4, "fill" => connected ? @colors[:text] : @colors[:muted],
|
|
447
791
|
"text-anchor" => anchor, data_pin_label: "#{component.ref}.#{pin.name}")
|
|
448
792
|
end
|
|
449
|
-
|
|
793
|
+
opacity = @emphasis_active && !emphasized_component?(component.ref) ? " opacity=\"0.18\"" : ""
|
|
794
|
+
layer_group("<g data-ref=\"#{escape(component.ref)}\"#{opacity}>#{out.join}</g>", read(component.attrs, "layer"))
|
|
450
795
|
end.join
|
|
451
796
|
end
|
|
452
797
|
|
|
@@ -481,15 +826,10 @@ module Breadkit
|
|
|
481
826
|
@circuit.wires.each do |wire|
|
|
482
827
|
from, to = endpoint_point(wire.from), endpoint_point(wire.to)
|
|
483
828
|
next unless from && to
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
elsif wire.route == "edge"
|
|
489
|
-
edge_route(wire, from, to)
|
|
490
|
-
else
|
|
491
|
-
"M #{fmt(px(from[0]))} #{fmt(py(from[1]))} L #{fmt(px(to[0]))} #{fmt(py(to[1]))}"
|
|
492
|
-
end
|
|
829
|
+
chosen = @color_by == "net" ? wire_color(wire) : (wire.color || wire_color(wire))
|
|
830
|
+
color = @theme == "print" ? "#222222" : render_color(chosen, "wire #{wire.id}") { wire_color(wire) }
|
|
831
|
+
color = { "removed" => "#d94c48", "added" => "#23a86b" }.fetch(@diff_wires[wire.id], color)
|
|
832
|
+
path = wire_path(wire, from, to)
|
|
493
833
|
dash = if wire.dashed
|
|
494
834
|
" stroke-dasharray=\"5 4\""
|
|
495
835
|
elsif @theme == "print"
|
|
@@ -497,26 +837,63 @@ module Breadkit
|
|
|
497
837
|
else
|
|
498
838
|
""
|
|
499
839
|
end
|
|
500
|
-
casing = "<path d=\"#{path}\" fill=\"none\" stroke=\"#{@colors[:board]}\" stroke-width=\"3.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>"
|
|
501
|
-
|
|
840
|
+
casing = @wire_style == "flat" ? "" : "<path d=\"#{path}\" fill=\"none\" stroke=\"#{@colors[:board]}\" stroke-width=\"3.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>"
|
|
841
|
+
net_attr = @nets_by_member[wire.id] ? " data-net=\"#{escape(@nets_by_member[wire.id].name)}\"" : ""
|
|
842
|
+
diff_attr = @diff_wires[wire.id] ? " data-diff=\"#{@diff_wires[wire.id]}\"" : ""
|
|
843
|
+
wire_svg = "<path d=\"#{path}\" fill=\"none\" stroke=\"#{escape(color)}\" stroke-width=\"#{@wire_style == 'flat' ? '1.8' : '2.4'}\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-ref=\"#{escape(wire.id)}\"#{net_attr}#{diff_attr}#{dash}/>"
|
|
502
844
|
dots = [from, to].uniq.map do |x, y|
|
|
503
845
|
circle(px(x), py(y), 2.8, fill: color, stroke: @colors[:board], stroke_width: 1.2,
|
|
504
846
|
data_wire: wire.id)
|
|
505
847
|
end
|
|
506
848
|
alternative = wire.electrical == false
|
|
507
|
-
casings << layer_group(casing, wire.layer, alternative: alternative)
|
|
508
|
-
lines << layer_group(wire_svg, wire.layer, alternative: alternative)
|
|
509
|
-
markers << layer_group(dots.join, wire.layer, alternative: alternative)
|
|
849
|
+
casings << layer_group(emphasis_group(casing, wire.id), wire.layer, alternative: alternative)
|
|
850
|
+
lines << layer_group(emphasis_group(wire_svg, wire.id), wire.layer, alternative: alternative)
|
|
851
|
+
markers << layer_group(emphasis_group(dots.join, wire.id), wire.layer, alternative: alternative)
|
|
510
852
|
end
|
|
511
853
|
(casings + lines + markers).join
|
|
512
854
|
end
|
|
513
855
|
|
|
856
|
+
def wire_path(wire, from, to)
|
|
857
|
+
if wire.route == "arc"
|
|
858
|
+
control_y = (py(from[1]) + py(to[1])) / 2.0 - 8
|
|
859
|
+
"M #{fmt(px(from[0]))} #{fmt(py(from[1]))} Q #{fmt((px(from[0]) + px(to[0])) / 2)} #{fmt(control_y)} #{fmt(px(to[0]))} #{fmt(py(to[1]))}"
|
|
860
|
+
elsif wire.route == "edge"
|
|
861
|
+
edge_route(wire, from, to)
|
|
862
|
+
elsif @wire_routing == "auto" && !offboard_component(wire.from) && !offboard_component(wire.to)
|
|
863
|
+
points = wire_route_points(wire, from, to)
|
|
864
|
+
"M #{points.map { |x, y| "#{fmt(px(x))} #{fmt(py(y))}" }.join(' L ')}"
|
|
865
|
+
else
|
|
866
|
+
"M #{fmt(px(from[0]))} #{fmt(py(from[1]))} L #{fmt(px(to[0]))} #{fmt(py(to[1]))}"
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
514
870
|
def layer_names(value)
|
|
515
871
|
Array(value).filter_map { |name| name.to_s unless name.nil? || name.to_s.empty? }
|
|
516
872
|
end
|
|
517
873
|
|
|
874
|
+
def emphasized_component?(ref)
|
|
875
|
+
@focus_ref ? ref == @focus_ref : @emphasized_component_refs.include?(ref)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def emphasis_group(svg, wire_id)
|
|
879
|
+
return svg unless @emphasis_active && !@emphasized_wire_ids.include?(wire_id)
|
|
880
|
+
|
|
881
|
+
"<g opacity=\"0.18\">#{svg}</g>"
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def visible_in_layer?(names)
|
|
885
|
+
layers = layer_names(names)
|
|
886
|
+
!@active_layer || layers.empty? || layers.include?(@active_layer)
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def visible_net?(net)
|
|
890
|
+
!@active_layer || net.holes.any? { |id| @used_holes.include?(id) } ||
|
|
891
|
+
net.members.any? { |member| @visible_wire_ids.include?(member) }
|
|
892
|
+
end
|
|
893
|
+
|
|
518
894
|
def layer_group(content, names, alternative: false)
|
|
519
895
|
names = layer_names(names)
|
|
896
|
+
return names.include?(@active_layer) ? content : "" if @active_layer && !names.empty?
|
|
520
897
|
return content if names.empty?
|
|
521
898
|
|
|
522
899
|
attributes = "data-layers=\"#{escape(JSON.generate(names))}\""
|
|
@@ -610,7 +987,7 @@ module Breadkit
|
|
|
610
987
|
|
|
611
988
|
def nets_svg
|
|
612
989
|
return "" unless @show_nets
|
|
613
|
-
@
|
|
990
|
+
@nets.select { |net| visible_net?(net) }.map do |net|
|
|
614
991
|
hole = @circuit.board.hole(net.holes.first)
|
|
615
992
|
next unless hole
|
|
616
993
|
display = display_hole(hole)
|
|
@@ -636,6 +1013,11 @@ module Breadkit
|
|
|
636
1013
|
out << text(x + 15, y, net.name, "font-size" => 5.5)
|
|
637
1014
|
y += 9
|
|
638
1015
|
end
|
|
1016
|
+
dip_pin_map_lines.each do |pin_line|
|
|
1017
|
+
out << text(x, y, pin_line, "font-size" => 5.5)
|
|
1018
|
+
y += 7
|
|
1019
|
+
end
|
|
1020
|
+
y += 2 unless dip_pin_map_lines.empty?
|
|
639
1021
|
end
|
|
640
1022
|
@annotations.each_with_index do |item, index|
|
|
641
1023
|
message = read(item, "message") || read(item, "rule")
|
|
@@ -652,11 +1034,35 @@ module Breadkit
|
|
|
652
1034
|
return "#222222" if @theme == "print"
|
|
653
1035
|
|
|
654
1036
|
wire = net.members.filter_map { |member| @wires_by_id[member] }.first
|
|
655
|
-
|
|
1037
|
+
chosen = @color_by == "net" ? wire_color(wire) : (wire&.color || wire_color(wire)) if wire
|
|
1038
|
+
wire ? render_color(chosen, "wire #{wire.id}") { wire_color(wire) } : wire_palette[@net_indexes.fetch(net) % wire_palette.length]
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def render_color(value, context)
|
|
1042
|
+
valid = if Breadkit.const_defined?(:Color)
|
|
1043
|
+
Breadkit::Color.valid?(value)
|
|
1044
|
+
else
|
|
1045
|
+
CSS_COLOR_NAMES.include?(value.to_s.downcase) || /\A#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\z/i.match?(value.to_s)
|
|
1046
|
+
end
|
|
1047
|
+
return value if valid
|
|
1048
|
+
|
|
1049
|
+
fallback = yield
|
|
1050
|
+
unless @invalid_colors_warned.include?(context)
|
|
1051
|
+
warn "bkrender: invalid color #{value.inspect} for #{context}; using #{fallback}"
|
|
1052
|
+
@invalid_colors_warned << context
|
|
1053
|
+
end
|
|
1054
|
+
fallback
|
|
656
1055
|
end
|
|
657
1056
|
|
|
658
1057
|
def legend_nets
|
|
659
|
-
@
|
|
1058
|
+
@nets.select { |net| visible_net?(net) && !(net.members.length == 1 && net.holes.empty?) }
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def dip_pin_map_lines
|
|
1062
|
+
@visible_components.select { |component| component.part.data.dig("render", "shape") == "dip" }.flat_map do |component|
|
|
1063
|
+
names = component.pins.values.sort_by { |pin| pin.number.to_i }.map { |pin| "#{pin.number} #{pin.name}" }
|
|
1064
|
+
annotation_lines("#{component.ref} pins: #{names.join(' · ')}")
|
|
1065
|
+
end
|
|
660
1066
|
end
|
|
661
1067
|
|
|
662
1068
|
def annotations_svg
|
|
@@ -684,41 +1090,70 @@ module Breadkit
|
|
|
684
1090
|
x, y = px(point[0]), py(point[1])
|
|
685
1091
|
anchor ||= [x, y]
|
|
686
1092
|
out << circle(x, y, 5, fill: "none", stroke: color, stroke_width: 1.5)
|
|
1093
|
+
annotation_state = read(item, "state")
|
|
1094
|
+
if read(item, "rule") == "Electrical/ShortCircuit" && @visible_wire_ids.include?(id) &&
|
|
1095
|
+
(annotation_state.nil? || annotation_state.to_s == @state&.name.to_s)
|
|
1096
|
+
finish = endpoint_point(wire.to)
|
|
1097
|
+
out << tag("path", d: wire_path(wire, point, finish), fill: "none", stroke: color,
|
|
1098
|
+
stroke_width: 5, stroke_linecap: "round", stroke_dasharray: "5 4", opacity: 0.85,
|
|
1099
|
+
data_short_path: id) if finish
|
|
1100
|
+
end
|
|
687
1101
|
end
|
|
688
1102
|
end
|
|
1103
|
+
Array(read(targets, "pins")).each do |ref|
|
|
1104
|
+
point = endpoint_point(ref)
|
|
1105
|
+
next unless point
|
|
1106
|
+
|
|
1107
|
+
x, y = px(point[0]), py(point[1])
|
|
1108
|
+
anchor ||= [x, y]
|
|
1109
|
+
out << circle(x, y, 4, fill: "none", stroke: color, stroke_width: 1.5, data_pin: ref)
|
|
1110
|
+
end
|
|
689
1111
|
Array(read(targets, "components")).each do |ref|
|
|
690
1112
|
component = @circuit.components[ref]
|
|
1113
|
+
next unless component
|
|
1114
|
+
|
|
1115
|
+
if component.part.placement == "offboard"
|
|
1116
|
+
x, y, width, height, = offboard_layout(component)
|
|
1117
|
+
left, top = px(x), py(y + height)
|
|
1118
|
+
anchor ||= [left + px(width) / 2, top + px(height) / 2]
|
|
1119
|
+
out << rect(left - 3, top - 3, px(width) + 6, px(height) + 6,
|
|
1120
|
+
fill: "none", stroke: color, stroke_width: 1.5, data_component: ref)
|
|
1121
|
+
next
|
|
1122
|
+
end
|
|
691
1123
|
holes = component && component.pins.values.map { |pin| @circuit.board.hole(pin.hole_id) }.compact
|
|
692
|
-
next if holes.
|
|
1124
|
+
next if holes.empty?
|
|
693
1125
|
xs, ys = holes.map { |hole| px(display_hole(hole).x) }, holes.map { |hole| py(display_hole(hole).y) }
|
|
694
1126
|
anchor ||= [(xs.min + xs.max) / 2, (ys.min + ys.max) / 2]
|
|
695
1127
|
out << rect(xs.min - 7, ys.min - 7, xs.max - xs.min + 14, ys.max - ys.min + 14,
|
|
696
|
-
fill: "none", stroke: color, stroke_width: 1.5)
|
|
1128
|
+
fill: "none", stroke: color, stroke_width: 1.5, data_component: ref)
|
|
697
1129
|
end
|
|
698
1130
|
Array(read(targets, "nets")).each do |name|
|
|
699
1131
|
state_name = read(item, "state")
|
|
1132
|
+
state = @state
|
|
700
1133
|
if state_name
|
|
701
|
-
@annotation_states ||=
|
|
702
|
-
state = @annotation_states
|
|
1134
|
+
@annotation_states ||= {}
|
|
1135
|
+
state = @annotation_states.fetch(state_name) do
|
|
1136
|
+
@annotation_states[state_name] = StateSelection.resolve(@circuit, state_name)
|
|
1137
|
+
end
|
|
703
1138
|
next unless state
|
|
704
1139
|
end
|
|
705
1140
|
net = @circuit.nets(state).find { |candidate| candidate.name == name }
|
|
706
1141
|
next unless net
|
|
707
|
-
|
|
708
|
-
point = net.members.filter_map { |member| endpoint_point(member) }.first
|
|
709
|
-
if point
|
|
710
|
-
x, y = px(point[0]), py(point[1])
|
|
711
|
-
anchor ||= [x, y]
|
|
712
|
-
out << circle(x, y, 4, fill: "none", stroke: color, stroke_width: 1.5)
|
|
713
|
-
end
|
|
714
|
-
end
|
|
715
|
-
net.holes.each do |id|
|
|
1142
|
+
points = net.holes.filter_map do |id|
|
|
716
1143
|
hole = @circuit.board.hole(id)
|
|
717
|
-
|
|
718
|
-
display
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1144
|
+
display = display_hole(hole) if hole
|
|
1145
|
+
[px(display.x), py(display.y)] if display
|
|
1146
|
+
end
|
|
1147
|
+
points = net.members.filter_map { |member| endpoint_point(member)&.then { |x, y| [px(x), py(y)] } } if points.empty?
|
|
1148
|
+
next if points.empty?
|
|
1149
|
+
|
|
1150
|
+
xs, ys = points.map(&:first), points.map(&:last)
|
|
1151
|
+
anchor ||= points.first
|
|
1152
|
+
if points.length == 1
|
|
1153
|
+
out << circle(xs.first, ys.first, 4, fill: "none", stroke: color, stroke_width: 1.5, data_net: name)
|
|
1154
|
+
else
|
|
1155
|
+
out << rect(xs.min - 4, ys.min - 4, xs.max - xs.min + 8, ys.max - ys.min + 8,
|
|
1156
|
+
rx: 4, fill: "none", stroke: color, stroke_width: 1.2, data_net: name)
|
|
722
1157
|
end
|
|
723
1158
|
end
|
|
724
1159
|
if anchor
|
|
@@ -768,16 +1203,21 @@ module Breadkit
|
|
|
768
1203
|
end
|
|
769
1204
|
|
|
770
1205
|
def view_box(mode)
|
|
771
|
-
offboard = @
|
|
772
|
-
wire_points = @
|
|
1206
|
+
offboard = @visible_components.any? { |component| component.part.placement == "offboard" }
|
|
1207
|
+
wire_points = @visible_wires.flat_map do |wire|
|
|
773
1208
|
from, to = endpoint_point(wire.from), endpoint_point(wire.to)
|
|
774
1209
|
from && to ? wire_route_points(wire, from, to) : []
|
|
775
1210
|
end
|
|
776
1211
|
if mode == "none" || (@used_holes.empty? && !offboard)
|
|
777
1212
|
left, top, width, height = board_bounds
|
|
778
|
-
module_points = wire_points + @
|
|
1213
|
+
module_points = wire_points + @visible_components.flat_map do |component|
|
|
779
1214
|
if component.part.placement == "offboard"
|
|
780
1215
|
offboard_bounds(component)
|
|
1216
|
+
elsif rail_module?(component)
|
|
1217
|
+
pins = component.pins.values.filter_map { |pin| [pin, @circuit.board.hole(pin.hole_id)] if pin.hole_id }
|
|
1218
|
+
next [] if pins.empty?
|
|
1219
|
+
x, y, module_width, module_height = rail_module_bounds(pins)
|
|
1220
|
+
[[x, y], [x + module_width, y + module_height]]
|
|
781
1221
|
elsif component.part.data.dig("render", "shape") == "module"
|
|
782
1222
|
pins = component.pins.values.filter_map { |pin| [pin, @circuit.board.hole(pin.hole_id)] if pin.hole_id }
|
|
783
1223
|
next [] if pins.empty?
|
|
@@ -804,9 +1244,15 @@ module Breadkit
|
|
|
804
1244
|
[display.x, display.y] if display
|
|
805
1245
|
end
|
|
806
1246
|
points.concat(wire_points)
|
|
807
|
-
points.concat(@
|
|
1247
|
+
points.concat(@visible_components.select { |component| component.part.placement == "offboard" }
|
|
808
1248
|
.flat_map { |component| offboard_bounds(component) })
|
|
809
|
-
points.concat(@
|
|
1249
|
+
points.concat(@visible_components.flat_map do |component|
|
|
1250
|
+
if rail_module?(component)
|
|
1251
|
+
pins = component.pins.values.filter_map { |pin| [pin, @circuit.board.hole(pin.hole_id)] if pin.hole_id }
|
|
1252
|
+
next [] if pins.empty?
|
|
1253
|
+
x, y, width, height = rail_module_bounds(pins)
|
|
1254
|
+
next [[x, y], [x + width, y + height]]
|
|
1255
|
+
end
|
|
810
1256
|
next [] unless component.part.data.dig("render", "shape") == "module"
|
|
811
1257
|
pins = component.pins.values.filter_map { |pin| [pin, @circuit.board.hole(pin.hole_id)] if pin.hole_id }
|
|
812
1258
|
next [] if pins.empty?
|
|
@@ -821,7 +1267,7 @@ module Breadkit
|
|
|
821
1267
|
def legend_height
|
|
822
1268
|
return 0 unless @legend_enabled || !@annotations.empty?
|
|
823
1269
|
|
|
824
|
-
20 + (@legend_enabled ? legend_nets.length * 9 : 0) +
|
|
1270
|
+
20 + (@legend_enabled ? legend_nets.length * 9 + dip_pin_map_lines.length * 7 + (dip_pin_map_lines.empty? ? 0 : 2) : 0) +
|
|
825
1271
|
@annotations.each_with_index.sum { |item, index| annotation_lines("#{index + 1}. #{read(item, "message") || read(item, "rule")}").length * 7 + 2 }
|
|
826
1272
|
end
|
|
827
1273
|
|
|
@@ -837,12 +1283,13 @@ module Breadkit
|
|
|
837
1283
|
parsed = Breadkit::HoleId.parse(endpoint, board: @circuit.board)
|
|
838
1284
|
if parsed.kind == :pin
|
|
839
1285
|
component = @circuit.components[parsed.ref]
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1286
|
+
pin = component&.pin(parsed.pin)
|
|
1287
|
+
return unless pin
|
|
1288
|
+
return offboard_pin_point(component, component.pins.values.index(pin)) if component.part.placement == "offboard"
|
|
1289
|
+
|
|
1290
|
+
hole = @circuit.board.hole(pin.hole_id)
|
|
1291
|
+
display = display_hole(hole) if hole
|
|
1292
|
+
return [display.x, display.y] if display
|
|
846
1293
|
end
|
|
847
1294
|
hole = @circuit.board.hole(endpoint)
|
|
848
1295
|
display = display_hole(hole) if hole
|
|
@@ -861,11 +1308,41 @@ module Breadkit
|
|
|
861
1308
|
edge_route_points(wire, from, to)
|
|
862
1309
|
elsif wire.route == "arc"
|
|
863
1310
|
[from, [(from[0] + to[0]) / 2.0, (from[1] + to[1]) / 2.0 + 0.8], to]
|
|
1311
|
+
elsif @wire_routing == "auto" && !offboard_component(wire.from) && !offboard_component(wire.to)
|
|
1312
|
+
@auto_routes[wire.id] ||= @auto_router.route(from, to)
|
|
864
1313
|
else
|
|
865
1314
|
[from, to]
|
|
866
1315
|
end
|
|
867
1316
|
end
|
|
868
1317
|
|
|
1318
|
+
def auto_obstacles
|
|
1319
|
+
@visible_components.filter_map do |component|
|
|
1320
|
+
next if component.part.placement == "offboard" || rail_module?(component)
|
|
1321
|
+
pins = component.pins.values.filter_map { |pin| @circuit.board.hole(pin.hole_id) }
|
|
1322
|
+
next if pins.empty?
|
|
1323
|
+
|
|
1324
|
+
xs, ys = pins.map { |hole| display_hole(hole).x }, pins.map { |hole| display_hole(hole).y }
|
|
1325
|
+
shape = component.part.data.dig("render", "shape")
|
|
1326
|
+
if shape == "module"
|
|
1327
|
+
x, y, width, height = module_bounds(component, pins.map { |hole| [nil, hole] })
|
|
1328
|
+
[x, y, x + width, y + height]
|
|
1329
|
+
elsif (size = component.part.data.dig("render", "size_mm"))
|
|
1330
|
+
width, height = size.map { |value| value.to_f / 2.54 }
|
|
1331
|
+
center_x, center_y = (xs.min + xs.max) / 2.0, (ys.min + ys.max) / 2.0
|
|
1332
|
+
[center_x - width / 2, center_y - height / 2, center_x + width / 2, center_y + height / 2]
|
|
1333
|
+
elsif shape == "dip"
|
|
1334
|
+
[xs.min - 0.4, (ys.min + ys.max) / 2.0 - 0.4, xs.max + 0.4, (ys.min + ys.max) / 2.0 + 0.4]
|
|
1335
|
+
else
|
|
1336
|
+
half_width, half_height = { "resistor" => [1.2, 0.4], "led_5mm" => [0.55, 0.55],
|
|
1337
|
+
"tact_switch" => [1.2, 1.0], "potentiometer" => [1.2, 0.8],
|
|
1338
|
+
"diode" => [0.7, 0.3], "rgb_led_5mm" => [0.7, 0.7],
|
|
1339
|
+
"to92" => [0.8, 0.6] }.fetch(shape, [0.9, 0.5])
|
|
1340
|
+
center_x, center_y = (xs.min + xs.max) / 2.0, (ys.min + ys.max) / 2.0
|
|
1341
|
+
[center_x - half_width, center_y - half_height, center_x + half_width, center_y + half_height]
|
|
1342
|
+
end
|
|
1343
|
+
end
|
|
1344
|
+
end
|
|
1345
|
+
|
|
869
1346
|
def edge_route_points(wire, from, to)
|
|
870
1347
|
module_from = offboard_component(wire.from)
|
|
871
1348
|
module_to = offboard_component(wire.to)
|
|
@@ -897,7 +1374,12 @@ module Breadkit
|
|
|
897
1374
|
[module_point, [module_point[0], lane], [x, lane], [x, board_point[1]], board_point]
|
|
898
1375
|
else
|
|
899
1376
|
y = board_point[1] + breakout
|
|
900
|
-
|
|
1377
|
+
detour = landscape_terminal_detour(board_point[0], lane, y, index)
|
|
1378
|
+
if detour
|
|
1379
|
+
[module_point, [lane, module_point[1]], [lane, detour], [board_point[0], detour], [board_point[0], y], board_point]
|
|
1380
|
+
else
|
|
1381
|
+
[module_point, [lane, module_point[1]], [lane, y], [board_point[0], y], board_point]
|
|
1382
|
+
end
|
|
901
1383
|
end
|
|
902
1384
|
elsif hole&.kind == :rail && @orientation == "portrait"
|
|
903
1385
|
component = module_from || module_to
|
|
@@ -924,6 +1406,19 @@ module Breadkit
|
|
|
924
1406
|
points
|
|
925
1407
|
end
|
|
926
1408
|
|
|
1409
|
+
def landscape_terminal_detour(from_x, to_x, y, index)
|
|
1410
|
+
obstacles = @visible_components.filter_map do |component|
|
|
1411
|
+
next unless component.part.placement == "footprint" && component.part.data.dig("render", "shape") == "module"
|
|
1412
|
+
|
|
1413
|
+
pins = component.pins.values.filter_map { |pin| [pin, @circuit.board.hole(pin.hole_id)] if pin.hole_id }
|
|
1414
|
+
next if pins.empty?
|
|
1415
|
+
|
|
1416
|
+
x, bottom, width, height = module_bounds(component, pins)
|
|
1417
|
+
bottom + height if y.between?(bottom - 0.3, bottom + height + 0.3) && [from_x, to_x].min < x + width && [from_x, to_x].max > x
|
|
1418
|
+
end
|
|
1419
|
+
obstacles.max && obstacles.max + 1.2 + index * 0.35
|
|
1420
|
+
end
|
|
1421
|
+
|
|
927
1422
|
def edge_breakout_offset(wire, x)
|
|
928
1423
|
@edge_breakout_offsets ||= begin
|
|
929
1424
|
groups = Hash.new { |hash, key| hash[key] = [] }
|
|
@@ -1080,14 +1575,18 @@ module Breadkit
|
|
|
1080
1575
|
def wire_color(wire)
|
|
1081
1576
|
if @color_by == "net"
|
|
1082
1577
|
net = @nets_by_member[wire.id] || @nets_by_hole[wire.from]
|
|
1083
|
-
voltage = net && @circuit.potentials.values[net.name]
|
|
1578
|
+
voltage = net && @circuit.potentials(@state).values[net.name]
|
|
1084
1579
|
return voltage.positive? ? @colors[:positive] : (voltage.negative? ? @colors[:negative_wire] : @colors[:text]) if voltage
|
|
1085
1580
|
if net
|
|
1086
1581
|
hash = net.name.each_byte.reduce(2_166_136_261) { |value, byte| ((value ^ byte) * 16_777_619) & 0xffffffff }
|
|
1087
|
-
return
|
|
1582
|
+
return wire_palette[hash % wire_palette.length]
|
|
1088
1583
|
end
|
|
1089
1584
|
end
|
|
1090
|
-
|
|
1585
|
+
wire_palette[@wire_indexes.fetch(wire) % wire_palette.length]
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
def wire_palette
|
|
1589
|
+
@theme == "colorblind" ? COLORBLIND_PALETTE : PALETTE
|
|
1091
1590
|
end
|
|
1092
1591
|
|
|
1093
1592
|
def resistor_bands(value, bands: 4)
|
|
@@ -1128,6 +1627,10 @@ module Breadkit
|
|
|
1128
1627
|
format("%.2f", value.to_f)
|
|
1129
1628
|
end
|
|
1130
1629
|
|
|
1630
|
+
def coord(value)
|
|
1631
|
+
fmt(value).sub(/\.?0+\z/, "")
|
|
1632
|
+
end
|
|
1633
|
+
|
|
1131
1634
|
def escape(value)
|
|
1132
1635
|
CGI.escapeHTML(value.to_s)
|
|
1133
1636
|
end
|