@protoboardai/views 0.0.0-stage → 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.
Files changed (56) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +21 -2
  4. package/package.json +46 -4
  5. package/src/applies.ts +54 -0
  6. package/src/builtins.tsx +89 -0
  7. package/src/drawing/BoardDrawingView.tsx +515 -0
  8. package/src/drawing/CircuitPcbView.tsx +494 -0
  9. package/src/drawing/DrawingControls.tsx +39 -0
  10. package/src/drawing/boards.ts +82 -0
  11. package/src/drawing/circuit.ts +413 -0
  12. package/src/drawing/kicadSchematic.ts +277 -0
  13. package/src/drawing/palettes.ts +150 -0
  14. package/src/drawing/svg.ts +445 -0
  15. package/src/drawing/usePanZoom.ts +400 -0
  16. package/src/icons.tsx +69 -0
  17. package/src/index.ts +33 -0
  18. package/src/inspector/DrcPanel.tsx +302 -0
  19. package/src/inspector/SelectionInspector.tsx +392 -0
  20. package/src/inspector/index.ts +9 -0
  21. package/src/inspector/model.ts +224 -0
  22. package/src/pair/PairView.tsx +314 -0
  23. package/src/pair/model.ts +206 -0
  24. package/src/parts/BomEditor.tsx +564 -0
  25. package/src/parts/CardPages.tsx +317 -0
  26. package/src/parts/CategoryList.tsx +72 -0
  27. package/src/parts/CloudPanel.tsx +160 -0
  28. package/src/parts/GlbPreview.tsx +116 -0
  29. package/src/parts/LibrariesManager.tsx +251 -0
  30. package/src/parts/LibraryBrowser.tsx +191 -0
  31. package/src/parts/LibraryPage.tsx +168 -0
  32. package/src/parts/PartCard.tsx +274 -0
  33. package/src/parts/PartDetail.tsx +613 -0
  34. package/src/parts/PartStatus.tsx +54 -0
  35. package/src/parts/Prices.tsx +241 -0
  36. package/src/parts/ProjectParts.tsx +174 -0
  37. package/src/parts/ProjectPartsPage.tsx +264 -0
  38. package/src/parts/SettingsPage.tsx +268 -0
  39. package/src/parts/Surface.tsx +51 -0
  40. package/src/parts/Windowed.tsx +70 -0
  41. package/src/parts/carousel.tsx +344 -0
  42. package/src/parts/index.ts +32 -0
  43. package/src/parts/library-data.ts +726 -0
  44. package/src/parts/matte.ts +69 -0
  45. package/src/parts/model.ts +590 -0
  46. package/src/parts/part-pages.ts +379 -0
  47. package/src/parts/part-status.ts +88 -0
  48. package/src/parts/revision.ts +11 -0
  49. package/src/parts/three.d.ts +5 -0
  50. package/src/parts/ui.tsx +335 -0
  51. package/src/parts/views.tsx +433 -0
  52. package/src/registry.ts +107 -0
  53. package/src/tabs.ts +118 -0
  54. package/src/theme.ts +225 -0
  55. package/src/types.ts +93 -0
  56. package/src/ui/Select.tsx +99 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * A routed board as the PCB view draws it, read from the circuit provider's
3
+ * own result (`circuit.json`, Circuit JSON: what `circuit:generate` built and
4
+ * routed). Nothing is re-routed or re-placed here: every pad, trace, via and
5
+ * pour is drawn where the file puts it.
6
+ *
7
+ * The model sorts the board into layers the view turns on and off one by one
8
+ * (each copper layer, silkscreen, courtyard and fabrication on each side,
9
+ * drills, the board edge), knows which net every piece of copper belongs to
10
+ * and what that net is called, which component every pad belongs to, and
11
+ * where a finding of the board's check is on the board.
12
+ *
13
+ * Board coordinates are millimetres with y up; the model's are the drawing's:
14
+ * millimetres with y down (`y` negated), so an SVG draws them as they are.
15
+ * Rotations are the drawing's too (clockwise on screen).
16
+ *
17
+ * No DOM here: it runs in Node for tests and in any shell.
18
+ */
19
+
20
+ /** One element of Circuit JSON; only the fields read here are typed. */
21
+ export type CircuitElement = { type: string; [key: string]: unknown };
22
+
23
+ export type Side = "top" | "bottom";
24
+
25
+ /** A layer the view can turn on and off. */
26
+ export type LayerId = `copper:${string}` | `silk:${Side}` | `courtyard:${Side}` | `fab:${Side}` | "drill" | "edge" | "keepout";
27
+
28
+ export type Shape =
29
+ | { kind: "rect"; x: number; y: number; w: number; h: number; rotate?: number; r?: number }
30
+ | { kind: "circle"; x: number; y: number; r: number }
31
+ /** A stroked line through points (a trace run, a silkscreen path). */
32
+ | { kind: "line"; points: [number, number][]; width: number }
33
+ /** A filled outline, with holes (a copper pour, a board outline). */
34
+ | { kind: "area"; rings: [number, number][][] }
35
+ | { kind: "text"; x: number; y: number; text: string; size: number; rotate?: number; anchor?: "start" | "middle" | "end" };
36
+
37
+ export interface Piece {
38
+ shape: Shape;
39
+ /** The net's key (several names may join on one net; the key is the net). */
40
+ net?: string;
41
+ /** The component it belongs to, by its reference (`u1`). */
42
+ ref?: string;
43
+ /** A pad's pin name (`GND`, `pin3`). */
44
+ pin?: string;
45
+ /** Outline only (silkscreen, courtyard, fabrication drawings): `stroke` width, no fill. */
46
+ stroke?: number;
47
+ }
48
+
49
+ export interface Net {
50
+ key: string;
51
+ /** What people call it: the source net's name; else the first pad on it (`u1.GND`). */
52
+ name: string;
53
+ /** Pads on it, as `ref.pin`. */
54
+ pads: string[];
55
+ }
56
+
57
+ export interface Component {
58
+ ref: string;
59
+ /** Centre and size on the board (drawing coordinates), rotation (clockwise on screen, degrees). */
60
+ x: number;
61
+ y: number;
62
+ w: number;
63
+ h: number;
64
+ rotate: number;
65
+ side: Side;
66
+ /** The manufacturer part number the source gives. */
67
+ mpn?: string;
68
+ }
69
+
70
+ export interface Layer {
71
+ id: LayerId;
72
+ label: string;
73
+ /** The theme role it is drawn in (`--pb-d-<role>`). */
74
+ role: string;
75
+ pieces: Piece[];
76
+ }
77
+
78
+ export interface PcbModel {
79
+ /** Layers in drawing order, bottom-most first. Copper is top, inner…, bottom by `copperLayers`. */
80
+ layers: Layer[];
81
+ /** Copper layer names, top first: top, inner1, inner2, …, bottom. */
82
+ copperLayers: string[];
83
+ /** The board's box (drawing coordinates): x, y, width, height. */
84
+ box: [number, number, number, number];
85
+ nets: Map<string, Net>;
86
+ components: Map<string, Component>;
87
+ /** The errors and warnings the provider recorded in the file, with where they are when it says. */
88
+ recorded: { type: string; message: string; at?: [number, number] }[];
89
+ }
90
+
91
+ const num = (v: unknown, d = 0) => (typeof v === "number" && Number.isFinite(v) ? v : d);
92
+ const str = (v: unknown) => (typeof v === "string" ? v : undefined);
93
+ const pt = (p: unknown): [number, number] => {
94
+ const o = (p ?? {}) as { x?: unknown; y?: unknown };
95
+ return [num(o.x), -num(o.y)];
96
+ };
97
+ /** A board rotation (counter-clockwise, y up) as the drawing's (clockwise on screen, y down): the same number negated. */
98
+ const rot = (deg: unknown) => -num(deg);
99
+
100
+ const COPPER_ORDER = (a: string, b: string) => {
101
+ const rank = (l: string) => (l === "top" ? -1 : l === "bottom" ? 1e6 : Number(/^inner(\d+)$/.exec(l)?.[1] ?? 1e5));
102
+ return rank(a) - rank(b);
103
+ };
104
+
105
+ /** The copper layer's name for people: Top, Inner 1, Bottom. */
106
+ export const copperLabel = (l: string) => (l === "top" ? "Top" : l === "bottom" ? "Bottom" : /^inner(\d+)$/.test(l) ? `Inner ${l.slice(5)}` : l);
107
+
108
+ /** Read a board's Circuit JSON. */
109
+ export function readCircuit(cj: CircuitElement[]): PcbModel {
110
+ const of = (type: string) => cj.filter((e) => e.type === type);
111
+ const byId = (type: string, idKey: string) => new Map(of(type).map((e) => [String(e[idKey]), e]));
112
+
113
+ // ---- components: reference, side, place
114
+ const sourceComponents = byId("source_component", "source_component_id");
115
+ const pcbComponents = byId("pcb_component", "pcb_component_id");
116
+ const refOfPcb = new Map<string, string>();
117
+ const components = new Map<string, Component>();
118
+ for (const [id, c] of pcbComponents) {
119
+ const sc = sourceComponents.get(String(c.source_component_id));
120
+ const ref = str(sc?.name) ?? id;
121
+ refOfPcb.set(id, ref);
122
+ const [x, y] = pt(c.center);
123
+ components.set(ref, { ref, x, y, w: num(c.width), h: num(c.height), rotate: rot(c.rotation), side: c.layer === "bottom" ? "bottom" : "top", ...(str(sc?.manufacturer_part_number) ? { mpn: str(sc?.manufacturer_part_number) } : {}) });
124
+ }
125
+
126
+ // ---- nets: the connectivity key the provider wrote on every port, net and trace
127
+ const sourcePorts = byId("source_port", "source_port_id");
128
+ const pcbPorts = byId("pcb_port", "pcb_port_id");
129
+ const sourceNets = of("source_net");
130
+ const sourceTraces = byId("source_trace", "source_trace_id");
131
+ const netNames = new Map<string, string>();
132
+ for (const n of sourceNets) {
133
+ const key = str(n.subcircuit_connectivity_map_key) ?? String(n.source_net_id);
134
+ if (!netNames.has(key) && str(n.name)) netNames.set(key, String(n.name));
135
+ }
136
+ const netKeyOfSourceNet = new Map(sourceNets.map((n) => [String(n.source_net_id), str(n.subcircuit_connectivity_map_key) ?? String(n.source_net_id)]));
137
+ const nets = new Map<string, Net>();
138
+ const portInfo = (pcbPortId: unknown): { net?: string; ref?: string; pin?: string } => {
139
+ const pp = pcbPorts.get(String(pcbPortId));
140
+ if (!pp) return {};
141
+ const sp = sourcePorts.get(String(pp.source_port_id));
142
+ const ref = refOfPcb.get(String(pp.pcb_component_id)) ?? str(sourceComponents.get(String(sp?.source_component_id))?.name);
143
+ const pin = str(sp?.name) ?? (sp?.pin_number !== undefined ? `pin${String(sp.pin_number)}` : undefined);
144
+ const net = str(sp?.subcircuit_connectivity_map_key);
145
+ if (net && ref && pin) {
146
+ const n = nets.get(net) ?? { key: net, name: "", pads: [] };
147
+ if (!n.pads.includes(`${ref}.${pin}`)) n.pads.push(`${ref}.${pin}`);
148
+ nets.set(net, n);
149
+ }
150
+ return { net, ref, pin };
151
+ };
152
+ const traceNet = (t: CircuitElement): string | undefined => {
153
+ const st = sourceTraces.get(String(t.source_trace_id));
154
+ const k = str(st?.subcircuit_connectivity_map_key) ?? str(t.subcircuit_connectivity_map_key);
155
+ if (k) return k;
156
+ const cn = str(t.connection_name);
157
+ if (cn && netKeyOfSourceNet.has(cn)) return netKeyOfSourceNet.get(cn);
158
+ for (const p of (t.route ?? []) as Record<string, unknown>[]) {
159
+ const n = portInfo(p.start_pcb_port_id ?? p.end_pcb_port_id).net;
160
+ if (n) return n;
161
+ }
162
+ return undefined;
163
+ };
164
+
165
+ // ---- layers
166
+ const copperNames = new Set<string>(["top", "bottom"]);
167
+ const board = of("pcb_board")[0];
168
+ const count = num(board?.num_layers, 2);
169
+ for (let i = 1; i <= count - 2; i++) copperNames.add(`inner${i}`);
170
+ const copper = new Map<string, Piece[]>();
171
+ const onCopper = (layer: string, p: Piece) => {
172
+ copperNames.add(layer);
173
+ copper.set(layer, [...(copper.get(layer) ?? []), p]);
174
+ };
175
+ const side = (l: unknown): Side => (l === "bottom" ? "bottom" : "top");
176
+ const silk: Record<Side, Piece[]> = { top: [], bottom: [] };
177
+ const courtyard: Record<Side, Piece[]> = { top: [], bottom: [] };
178
+ const fab: Record<Side, Piece[]> = { top: [], bottom: [] };
179
+ const drill: Piece[] = [];
180
+ const keepout: Piece[] = [];
181
+ const edge: Piece[] = [];
182
+
183
+ // pads
184
+ for (const p of of("pcb_smtpad")) {
185
+ const info = portInfo(p.pcb_port_id);
186
+ const ref = info.ref ?? refOfPcb.get(String(p.pcb_component_id));
187
+ const [x, y] = [num(p.x), -num(p.y)];
188
+ const shape = String(p.shape);
189
+ let s: Shape;
190
+ if (shape === "circle") s = { kind: "circle", x, y, r: num(p.radius) };
191
+ else if (shape === "polygon") s = { kind: "area", rings: [((p.points ?? []) as unknown[]).map(pt)] };
192
+ else {
193
+ const w = num(p.width);
194
+ const h = num(p.height);
195
+ const pill = /pill/.test(shape);
196
+ s = { kind: "rect", x, y, w, h, ...(p.ccw_rotation !== undefined ? { rotate: rot(p.ccw_rotation) } : {}), ...(pill ? { r: num(p.radius, Math.min(w, h) / 2) } : num(p.corner_radius) ? { r: num(p.corner_radius) } : {}) };
197
+ }
198
+ onCopper(String(p.layer ?? "top"), { shape: s, ...(info.net ? { net: info.net } : {}), ...(ref ? { ref } : {}), ...(info.pin ? { pin: info.pin } : {}) });
199
+ }
200
+ // plated holes: a pad on every copper layer, and a drill
201
+ for (const h of of("pcb_plated_hole")) {
202
+ const info = portInfo(h.pcb_port_id);
203
+ const ref = info.ref ?? refOfPcb.get(String(h.pcb_component_id));
204
+ const [x, y] = [num(h.x), -num(h.y)];
205
+ const shape = String(h.shape);
206
+ const rotate = h.ccw_rotation !== undefined ? rot(h.ccw_rotation) : undefined;
207
+ let pad: Shape;
208
+ let hole: Shape;
209
+ if (shape === "circle") {
210
+ pad = { kind: "circle", x, y, r: num(h.outer_diameter) / 2 };
211
+ hole = { kind: "circle", x, y, r: num(h.hole_diameter) / 2 };
212
+ } else if (/rect_pad/.test(shape)) {
213
+ pad = { kind: "rect", x, y, w: num(h.rect_pad_width), h: num(h.rect_pad_height), ...(rotate !== undefined ? { rotate } : {}), ...(num(h.rect_border_radius) ? { r: num(h.rect_border_radius) } : {}) };
214
+ hole = /pill/.test(shape)
215
+ ? { kind: "rect", x, y, w: num(h.hole_width), h: num(h.hole_height), r: Math.min(num(h.hole_width), num(h.hole_height)) / 2, ...(rotate !== undefined ? { rotate } : {}) }
216
+ : { kind: "circle", x, y, r: num(h.hole_diameter) / 2 };
217
+ } else {
218
+ const ow = num(h.outer_width);
219
+ const oh = num(h.outer_height);
220
+ pad = { kind: "rect", x, y, w: ow, h: oh, r: Math.min(ow, oh) / 2, ...(rotate !== undefined ? { rotate } : {}) };
221
+ hole = { kind: "rect", x, y, w: num(h.hole_width), h: num(h.hole_height), r: Math.min(num(h.hole_width), num(h.hole_height)) / 2, ...(rotate !== undefined ? { rotate } : {}) };
222
+ }
223
+ const layers = (h.layers as string[] | undefined) ?? [...copperNames];
224
+ for (const l of layers) onCopper(l, { shape: pad, ...(info.net ? { net: info.net } : {}), ...(ref ? { ref } : {}), ...(info.pin ? { pin: info.pin } : {}) });
225
+ drill.push({ shape: hole, ...(ref ? { ref } : {}) });
226
+ }
227
+ for (const h of of("pcb_hole")) {
228
+ const [x, y] = [num(h.x), -num(h.y)];
229
+ const shape = String(h.hole_shape ?? h.shape ?? "circle");
230
+ drill.push({ shape: shape === "circle" || shape === "round" ? { kind: "circle", x, y, r: num(h.hole_diameter) / 2 } : { kind: "rect", x, y, w: num(h.hole_width), h: num(h.hole_height), r: shape === "rect" ? 0 : Math.min(num(h.hole_width), num(h.hole_height)) / 2 } });
231
+ }
232
+ // traces: one stroked run per stretch on one layer
233
+ const traceNetById = new Map<string, string>();
234
+ for (const t of of("pcb_trace")) {
235
+ const net = traceNet(t);
236
+ if (net) traceNetById.set(String(t.pcb_trace_id), net);
237
+ let run: { layer: string; points: [number, number][]; width: number } | undefined;
238
+ const flush = () => {
239
+ if (run && run.points.length > 1) onCopper(run.layer, { shape: { kind: "line", points: run.points, width: run.width }, ...(net ? { net } : {}) });
240
+ run = undefined;
241
+ };
242
+ for (const p of (t.route ?? []) as Record<string, unknown>[]) {
243
+ if (p.route_type !== "wire") {
244
+ flush();
245
+ continue;
246
+ }
247
+ const layer = String(p.layer ?? "top");
248
+ const width = num(p.width, 0.15);
249
+ if (run && (run.layer !== layer || run.width !== width)) {
250
+ const last = run.points[run.points.length - 1]!;
251
+ flush();
252
+ run = { layer, points: [last], width };
253
+ }
254
+ run ??= { layer, points: [], width };
255
+ run.points.push([num(p.x), -num(p.y)]);
256
+ }
257
+ flush();
258
+ }
259
+ // vias: copper on every layer they join, and a drill
260
+ for (const v of of("pcb_via")) {
261
+ const [x, y] = [num(v.x), -num(v.y)];
262
+ const net = str(v.subcircuit_connectivity_map_key) ?? traceNetById.get(String(v.pcb_trace_id));
263
+ const layers = (v.layers as string[] | undefined) ?? [String(v.from_layer ?? "top"), String(v.to_layer ?? "bottom")];
264
+ for (const l of layers) onCopper(l, { shape: { kind: "circle", x, y, r: num(v.outer_diameter, 0.6) / 2 }, ...(net ? { net } : {}) });
265
+ drill.push({ shape: { kind: "circle", x, y, r: num(v.hole_diameter, 0.3) / 2 } });
266
+ }
267
+ // pours
268
+ for (const p of of("pcb_copper_pour")) {
269
+ const net = netKeyOfSourceNet.get(String(p.source_net_id)) ?? str(p.subcircuit_connectivity_map_key);
270
+ let s: Shape | undefined;
271
+ const brep = p.brep_shape as { outer_ring?: { vertices?: unknown[] }; inner_rings?: { vertices?: unknown[] }[] } | undefined;
272
+ if (brep?.outer_ring?.vertices) s = { kind: "area", rings: [brep.outer_ring.vertices.map(pt), ...(brep.inner_rings ?? []).map((r) => (r.vertices ?? []).map(pt))] };
273
+ else if (Array.isArray(p.points)) s = { kind: "area", rings: [(p.points as unknown[]).map(pt)] };
274
+ else if (p.shape === "rect") s = { kind: "rect", x: pt(p.center)[0], y: pt(p.center)[1], w: num(p.width), h: num(p.height), ...(p.rotation !== undefined ? { rotate: rot(p.rotation) } : {}) };
275
+ if (s) onCopper(String(p.layer ?? "top"), { shape: s, ...(net ? { net } : {}) });
276
+ }
277
+
278
+ // silkscreen
279
+ for (const e of cj) {
280
+ if (!e.type.startsWith("pcb_silkscreen_") && !e.type.startsWith("pcb_fabrication_note_") && !e.type.startsWith("pcb_courtyard_")) continue;
281
+ const ref = refOfPcb.get(String(e.pcb_component_id));
282
+ const target = e.type.startsWith("pcb_silkscreen_") ? silk : e.type.startsWith("pcb_courtyard_") ? courtyard : fab;
283
+ const sd = side(e.layer);
284
+ const w = num(e.stroke_width, e.type.startsWith("pcb_courtyard_") ? 0.05 : 0.1);
285
+ const kind = e.type.replace(/^pcb_(silkscreen|fabrication_note|courtyard)_/, "");
286
+ let s: Shape | undefined;
287
+ let stroke: number | undefined = w;
288
+ if (kind === "path") s = { kind: "line", points: ((e.route ?? []) as unknown[]).map(pt), width: w };
289
+ else if (kind === "line") s = { kind: "line", points: [[num(e.x1), -num(e.y1)], [num(e.x2), -num(e.y2)]], width: w };
290
+ else if (kind === "outline") s = { kind: "line", points: [...((e.outline ?? []) as unknown[]).map(pt), ...((e.outline as unknown[] | undefined)?.length ? [pt((e.outline as unknown[])[0])] : [])], width: w };
291
+ else if (kind === "rect") {
292
+ const [x, y] = pt(e.center);
293
+ s = { kind: "rect", x, y, w: num(e.width), h: num(e.height), ...(e.ccw_rotation !== undefined ? { rotate: rot(e.ccw_rotation) } : {}) };
294
+ } else if (kind === "circle") {
295
+ const [x, y] = pt(e.center);
296
+ s = { kind: "circle", x, y, r: num(e.radius) };
297
+ } else if (kind === "text") {
298
+ const [x, y] = pt(e.anchor_position);
299
+ const a = str(e.anchor_alignment) ?? "center";
300
+ s = { kind: "text", x, y, text: String(e.text ?? ""), size: num(e.font_size, 1), ...(e.ccw_rotation ? { rotate: rot(e.ccw_rotation) } : {}), anchor: /left/.test(a) ? "start" : /right/.test(a) ? "end" : "middle" };
301
+ stroke = undefined;
302
+ }
303
+ if (s) target[sd].push({ shape: s, ...(ref ? { ref } : {}), ...(stroke !== undefined ? { stroke } : {}) });
304
+ }
305
+ // the fabrication drawing: each component's body box and reference (a tool that writes its own notes adds to them)
306
+ for (const c of components.values()) {
307
+ fab[c.side].push({ shape: { kind: "rect", x: c.x, y: c.y, w: c.w, h: c.h, rotate: c.rotate }, ref: c.ref, stroke: 0.08 });
308
+ fab[c.side].push({ shape: { kind: "text", x: c.x, y: c.y, text: c.ref.toUpperCase(), size: Math.max(0.4, Math.min(1.2, Math.min(c.w, c.h) * 0.35)), anchor: "middle" }, ref: c.ref });
309
+ }
310
+ for (const k of of("pcb_keepout")) {
311
+ const [x, y] = pt(k.center);
312
+ keepout.push({ shape: k.shape === "circle" ? { kind: "circle", x, y, r: num(k.radius) } : { kind: "rect", x, y, w: num(k.width), h: num(k.height) }, stroke: 0.08 });
313
+ }
314
+
315
+ // the board: its outline, else its rectangle
316
+ let box: [number, number, number, number] = [0, 0, 1, 1];
317
+ if (board) {
318
+ const outline = (board.outline as unknown[] | undefined)?.map(pt);
319
+ const [cx, cy] = pt(board.center);
320
+ const shape: Shape = outline?.length ? { kind: "area", rings: [outline] } : { kind: "rect", x: cx, y: cy, w: num(board.width), h: num(board.height) };
321
+ edge.push({ shape, stroke: 0.1 });
322
+ const xs = outline?.length ? outline.map((p) => p[0]) : [cx - num(board.width) / 2, cx + num(board.width) / 2];
323
+ const ys = outline?.length ? outline.map((p) => p[1]) : [cy - num(board.height) / 2, cy + num(board.height) / 2];
324
+ box = [Math.min(...xs), Math.min(...ys), Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)];
325
+ }
326
+
327
+ for (const n of nets.values()) n.name = netNames.get(n.key) ?? n.pads[0] ?? n.key;
328
+ for (const [key, name] of netNames) if (!nets.has(key)) nets.set(key, { key, name, pads: [] });
329
+
330
+ // what the provider recorded: an error or a warning, at its place when it names one
331
+ const recorded: PcbModel["recorded"] = [];
332
+ for (const e of cj) {
333
+ if (!/_(error|warning)$/.test(e.type)) continue;
334
+ let at: [number, number] | undefined;
335
+ if (e.center) at = pt(e.center);
336
+ else if (e.x !== undefined && e.y !== undefined) at = [num(e.x), -num(e.y)];
337
+ else {
338
+ const ports = ((e.pcb_port_ids ?? []) as string[]).map((id) => pcbPorts.get(id)).filter(Boolean) as CircuitElement[];
339
+ if (ports.length) at = pt(ports[0]);
340
+ const comps = ((e.pcb_component_ids ?? (e.pcb_component_id ? [e.pcb_component_id] : [])) as string[]).map((id) => components.get(refOfPcb.get(id) ?? ""));
341
+ if (!at && comps[0]) at = [comps[0].x, comps[0].y];
342
+ const sc = e.source_component_id ? components.get(str(sourceComponents.get(String(e.source_component_id))?.name) ?? "") : undefined;
343
+ if (!at && sc) at = [sc.x, sc.y];
344
+ }
345
+ recorded.push({ type: e.type, message: String(e.message ?? e.type), ...(at ? { at } : {}) });
346
+ }
347
+
348
+ const copperLayers = [...copperNames].sort(COPPER_ORDER);
349
+ // drawn bottom-most first: the far side's copper under the near side's
350
+ const layers: Layer[] = [
351
+ { id: "edge", label: "Board edge", role: "edge", pieces: edge },
352
+ ...[...copperLayers].reverse().map<Layer>((l) => ({ id: `copper:${l}`, label: copperLabel(l), role: l === "top" ? "copper-top" : l === "bottom" ? "copper-bottom" : `copper-inner-${(Number(l.slice(5)) - 1) % 4}`, pieces: copper.get(l) ?? [] })),
353
+ { id: "courtyard:bottom", label: "Courtyard · bottom", role: "courtyard", pieces: courtyard.bottom },
354
+ { id: "fab:bottom", label: "Fab · bottom", role: "fab", pieces: fab.bottom },
355
+ { id: "silk:bottom", label: "Silkscreen · bottom", role: "silk-bottom", pieces: silk.bottom },
356
+ { id: "drill", label: "Drills", role: "drill", pieces: drill },
357
+ { id: "courtyard:top", label: "Courtyard · top", role: "courtyard", pieces: courtyard.top },
358
+ { id: "fab:top", label: "Fab · top", role: "fab", pieces: fab.top },
359
+ { id: "silk:top", label: "Silkscreen · top", role: "silk", pieces: silk.top },
360
+ { id: "keepout", label: "Keep-outs", role: "keepout", pieces: keepout },
361
+ ];
362
+ return { layers, copperLayers, box, nets, components, recorded };
363
+ }
364
+
365
+ /**
366
+ * The layers in the order they are drawn, bottom-most first, for a board seen
367
+ * from `side`: the board itself (its edge, filled) always first, then the far
368
+ * side's drawings, the copper from the far side to the near one, the drills,
369
+ * the near side's drawings and the keep-outs on top. (Reversing the top
370
+ * side's order for the bottom drew the filled board over everything.)
371
+ */
372
+ export function drawOrder(model: PcbModel, side: Side = "top"): Layer[] {
373
+ const by = new Map(model.layers.map((l) => [l.id, l]));
374
+ const near = side;
375
+ const far: Side = side === "top" ? "bottom" : "top";
376
+ const copper = model.copperLayers.map((l) => by.get(`copper:${l}`)).filter((l): l is Layer => !!l);
377
+ const pick = (ids: LayerId[]) => ids.map((id) => by.get(id)).filter((l): l is Layer => !!l);
378
+ const below = [...pick(["edge", `silk:${far}`, `fab:${far}`, `courtyard:${far}`]), ...(side === "top" ? [...copper].reverse() : copper)];
379
+ const above = pick(["drill", `courtyard:${near}`, `fab:${near}`, `silk:${near}`, "keepout"]);
380
+ // anything the reader adds later and these lists do not know goes between the copper and the drills
381
+ const rest = model.layers.filter((l) => !below.includes(l) && !above.includes(l));
382
+ return [...below, ...rest, ...above];
383
+ }
384
+
385
+ /**
386
+ * Where a finding of the board's check is, from what its message names: a
387
+ * point it gives (`at (1.20, -3.40)`), a piece of copper by its id, or a
388
+ * component by its reference. Undefined when it names no place (a stale
389
+ * build, a footprint not verified).
390
+ */
391
+ export function locate(message: string, model: PcbModel, cj?: CircuitElement[]): [number, number] | undefined {
392
+ const point = /\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)/.exec(message);
393
+ if (point) return [Number(point[1]), -Number(point[2])];
394
+ if (cj) {
395
+ for (const m of message.matchAll(/\b(pcb_(?:smtpad|via|plated_hole|hole|copper_pour|port)_\d+)\b/g)) {
396
+ const e = cj.find((x) => x[`${x.type}_id`] === m[1]);
397
+ if (e && e.x !== undefined) return [num(e.x), -num(e.y)];
398
+ if (e?.center) return pt(e.center);
399
+ }
400
+ }
401
+ // a component the message is about: its reference as a word
402
+ const words = message.split(/[^A-Za-z0-9_]+/).filter(Boolean);
403
+ for (const w of words) {
404
+ const c = model.components.get(w) ?? model.components.get(w.toLowerCase());
405
+ if (c) return [c.x, c.y];
406
+ }
407
+ return undefined;
408
+ }
409
+
410
+ /** The layer list's toggle for the board check's marks (B11): the marks are the findings with a place, said as such when some have none. */
411
+ export function findingsLabel(located: number, total: number): string {
412
+ return located === total ? `Findings · ${total}` : `Findings · ${located} of ${total}`;
413
+ }