@protoboardai/views 0.0.0-stage → 0.2.1

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 +57 -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,277 @@
1
+ /**
2
+ * What a KiCad schematic export (`kicad-cli sch export svg`, a locked board's
3
+ * schematic.svg) says once it is read for its geometry: KiCad writes no nets
4
+ * and no parts into the file, only strokes, with an invisible `<text>` beside
5
+ * each piece of text. From those:
6
+ *
7
+ * - wires are the strokes in the wire colour (`data-pb-stroke="wire"`, svg.ts);
8
+ * wires that meet (end to end, or an end on another wire) are one network;
9
+ * - a label (or a power symbol's name) is text whose anchor lies on a wire:
10
+ * it names that network, and networks with the same name are one net;
11
+ * - a reference designator (R20, U5) is text in the field colour that looks
12
+ * like one; the next field text after it is its value; a symbol body (the
13
+ * part fill) belongs to the reference nearest it.
14
+ *
15
+ * The geometry functions are plain data (tested in Node); `indexKicadSchematic`
16
+ * reads them from the drawing in the page.
17
+ */
18
+
19
+ export type Pt = [number, number];
20
+ export interface Segment {
21
+ a: Pt;
22
+ b: Pt;
23
+ }
24
+ export interface Label {
25
+ text: string;
26
+ /** The text's anchor and the far end of its run (the label's own line). */
27
+ at: Pt;
28
+ end: Pt;
29
+ }
30
+
31
+ /** The segments of a path's `d` (absolute moves and lines: what KiCad writes). */
32
+ export function segmentsOf(d: string): Segment[] {
33
+ const out: Segment[] = [];
34
+ let last: Pt | undefined;
35
+ let start: Pt | undefined;
36
+ for (const m of d.matchAll(/([MLZmlz])|(-?[\d.]+(?:e-?\d+)?)[\s,]+(-?[\d.]+(?:e-?\d+)?)/g)) {
37
+ if (m[1]) {
38
+ const c = m[1].toUpperCase();
39
+ if (c === "M") last = undefined;
40
+ if (c === "Z" && last && start) out.push({ a: last, b: start });
41
+ continue;
42
+ }
43
+ const p: Pt = [Number(m[2]), Number(m[3])];
44
+ if (last) out.push({ a: last, b: p });
45
+ else start = p;
46
+ last = p;
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /** Distance from a point to a segment. */
52
+ export function distance(p: Pt, s: Segment): number {
53
+ const [ax, ay] = s.a;
54
+ const dx = s.b[0] - ax;
55
+ const dy = s.b[1] - ay;
56
+ const len = dx * dx + dy * dy;
57
+ const t = len ? Math.max(0, Math.min(1, ((p[0] - ax) * dx + (p[1] - ay) * dy) / len)) : 0;
58
+ return Math.hypot(p[0] - (ax + t * dx), p[1] - (ay + t * dy));
59
+ }
60
+
61
+ /** A grid of segments by cell, for the near ones to a point. */
62
+ class Grid {
63
+ private cells = new Map<string, number[]>();
64
+ constructor(
65
+ private segs: Segment[],
66
+ private size = 5,
67
+ ) {
68
+ segs.forEach((s, i) => {
69
+ const [x0, x1] = [Math.min(s.a[0], s.b[0]), Math.max(s.a[0], s.b[0])];
70
+ const [y0, y1] = [Math.min(s.a[1], s.b[1]), Math.max(s.a[1], s.b[1])];
71
+ for (let x = Math.floor(x0 / size); x <= Math.floor(x1 / size); x++)
72
+ for (let y = Math.floor(y0 / size); y <= Math.floor(y1 / size); y++) {
73
+ const k = `${x},${y}`;
74
+ const c = this.cells.get(k);
75
+ if (c) c.push(i);
76
+ else this.cells.set(k, [i]);
77
+ }
78
+ });
79
+ }
80
+ near(p: Pt, r: number): number[] {
81
+ const out = new Set<number>();
82
+ for (let x = Math.floor((p[0] - r) / this.size); x <= Math.floor((p[0] + r) / this.size); x++)
83
+ for (let y = Math.floor((p[1] - r) / this.size); y <= Math.floor((p[1] + r) / this.size); y++) for (const i of this.cells.get(`${x},${y}`) ?? []) if (distance(p, this.segs[i]) <= r) out.add(i);
84
+ return [...out];
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The networks of a schematic's wires, each named by the labels on it: for
90
+ * each segment, the net it is on (its name, or `~<n>` for a network no label
91
+ * names), and for each label, its net.
92
+ */
93
+ export function wireNets(segs: Segment[], labels: Label[], tolerance = 0.05, reach = 0.6): { segmentNet: string[]; labelNet: (string | undefined)[] } {
94
+ const parent = segs.map((_, i) => i);
95
+ const find = (i: number): number => (parent[i] === i ? i : (parent[i] = find(parent[i])));
96
+ const join = (a: number, b: number) => {
97
+ const [x, y] = [find(a), find(b)];
98
+ if (x !== y) parent[x] = y;
99
+ };
100
+ const grid = new Grid(segs);
101
+ // an end of a wire on another wire (end to end, or a T) joins them
102
+ segs.forEach((s, i) => {
103
+ for (const p of [s.a, s.b]) for (const j of grid.near(p, tolerance)) if (j !== i) join(i, j);
104
+ });
105
+ // a label on a wire names its network (the anchor, or the end of its run, on the wire)
106
+ const named = new Map<number, string>();
107
+ const labelRoot = labels.map((l) => {
108
+ const hits = [...grid.near(l.at, reach), ...grid.near(l.end, reach)];
109
+ if (!hits.length) return undefined;
110
+ const r = find(hits[0]);
111
+ if (!named.has(r)) named.set(r, l.text);
112
+ return r;
113
+ });
114
+ // networks with the same name are one net
115
+ const byName = new Map<string, number>();
116
+ for (const [r, name] of named) {
117
+ const other = byName.get(name);
118
+ if (other === undefined) byName.set(name, r);
119
+ else join(r, other);
120
+ }
121
+ const nameOf = new Map<number, string>();
122
+ for (const [r, name] of named) nameOf.set(find(r), name);
123
+ const netOf = (i: number) => {
124
+ const r = find(i);
125
+ return nameOf.get(r) ?? `~${r}`;
126
+ };
127
+ return { segmentNet: segs.map((_, i) => netOf(i)), labelNet: labelRoot.map((r) => (r === undefined ? undefined : netOf(r))) };
128
+ }
129
+
130
+ /** A reference designator as KiCad writes one (R20, U5, BT1, SW1, D23); not a power symbol's `#PWR01`. */
131
+ export const isReference = (t: string) => /^[A-Z]{1,4}\d{1,4}[A-Z]?$/.test(t);
132
+
133
+ export interface SchematicPart {
134
+ ref: string;
135
+ value?: string;
136
+ at: Pt;
137
+ /** The elements that draw it (its reference, its value, its body), lit when it is picked. */
138
+ els: Element[];
139
+ /** Its body's box (drawing units), when it has one. */
140
+ box?: [number, number, number, number];
141
+ }
142
+
143
+ export interface SchematicIndex {
144
+ /** Each net's elements (wires, labels), by the net's key (its name, or `~<n>` for an unnamed one). */
145
+ nets: Map<string, Element[]>;
146
+ /** The net of an element the pointer can be over. */
147
+ netOf: Map<Element, string>;
148
+ parts: SchematicPart[];
149
+ /** The net nearest a drawing point within `reach` (a wire, or a label's run): strokes are too thin to hit with a pointer. */
150
+ netAt(p: Pt, reach: number): string | undefined;
151
+ }
152
+
153
+ /** The text an invisible `<text>` stands for, and the group of strokes KiCad drew for it (its next sibling). */
154
+ const drawnText = (t: Element): Element[] => {
155
+ const next = t.nextElementSibling;
156
+ return next?.matches("g.stroked-text") ? [t, next] : [t];
157
+ };
158
+
159
+ /** Read a KiCad schematic drawing in the page for its nets and parts. */
160
+ export function indexKicadSchematic(root: SVGSVGElement): SchematicIndex {
161
+ // the wires
162
+ const wirePaths = [...root.querySelectorAll('[data-pb-stroke="wire"] path, path[data-pb-stroke="wire"], [data-pb-stroke="wire"] polyline, polyline[data-pb-stroke="wire"]')].filter((e) => !e.closest(".stroked-text"));
163
+ const segs: Segment[] = [];
164
+ const segEl: Element[] = [];
165
+ for (const p of wirePaths) {
166
+ const d = p.getAttribute("d") ?? (p.getAttribute("points") ? `M ${p.getAttribute("points")}` : "");
167
+ for (const s of segmentsOf(d)) {
168
+ segs.push(s);
169
+ segEl.push(p);
170
+ }
171
+ }
172
+ // the symbols' bodies (the part fill): a pin's name is inside one, a reference never is
173
+ const bodies: { el: Element; box: [number, number, number, number] }[] = [];
174
+ for (const b of root.querySelectorAll('[data-pb-fill="body"]')) {
175
+ try {
176
+ const r = (b as SVGGraphicsElement).getBBox();
177
+ if (r.width && r.height) bodies.push({ el: b, box: [r.x, r.y, r.width, r.height] });
178
+ } catch {
179
+ /* not drawn */
180
+ }
181
+ }
182
+ const inBody = (x: number, y: number) => bodies.some(({ box: q }) => x > q[0] && x < q[0] + q[2] && y > q[1] && y < q[1] + q[3]);
183
+ // the texts: labels (lower-case names on a wire), references and values (the field colour)
184
+ const texts = [...root.querySelectorAll("text")];
185
+ const labels: Label[] = [];
186
+ const labelEls: Element[][] = [];
187
+ const parts: SchematicPart[] = [];
188
+ let lastPart: SchematicPart | undefined;
189
+ for (const t of texts) {
190
+ const text = t.textContent?.trim() ?? "";
191
+ if (!text) continue;
192
+ const x = parseFloat(t.getAttribute("x") ?? "");
193
+ const y = parseFloat(t.getAttribute("y") ?? "");
194
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
195
+ const field = t.closest('[data-pb-stroke="label"]');
196
+ if (field && isReference(text) && !inBody(x, y)) {
197
+ lastPart = { ref: text, at: [x, y], els: drawnText(t) };
198
+ parts.push(lastPart);
199
+ continue;
200
+ }
201
+ if (field && lastPart && !lastPart.value && !/^\d+$/.test(text)) {
202
+ lastPart.value = text;
203
+ lastPart.els.push(...drawnText(t));
204
+ lastPart = undefined;
205
+ continue;
206
+ }
207
+ lastPart = undefined;
208
+ if (/^\d+$/.test(text)) continue;
209
+ // a rotated label runs along its own axis: its far end is where the rotation takes the run's end
210
+ const len = parseFloat(t.getAttribute("textLength") ?? "0") || 0;
211
+ const rotated = /rotate\(\s*-?90/.test(t.closest("g[transform]")?.getAttribute("transform") ?? "");
212
+ const anchor = t.getAttribute("text-anchor");
213
+ const dir = anchor === "end" ? -1 : 1;
214
+ const end: Pt = rotated ? [x, y - dir * len] : [x + dir * len, y];
215
+ labels.push({ text, at: [x, y], end });
216
+ labelEls.push(drawnText(t));
217
+ }
218
+ const { segmentNet, labelNet } = wireNets(segs, labels);
219
+ const grid = new Grid(segs);
220
+ const nets = new Map<string, Element[]>();
221
+ const netOf = new Map<Element, string>();
222
+ const add = (net: string, els: Element[]) => {
223
+ const list = nets.get(net) ?? [];
224
+ for (const e of els) {
225
+ if (!netOf.has(e)) list.push(e);
226
+ netOf.set(e, net);
227
+ }
228
+ nets.set(net, list);
229
+ };
230
+ segmentNet.forEach((net, i) => add(net, [segEl[i]]));
231
+ labelNet.forEach((net, i) => net && add(net, labelEls[i]));
232
+
233
+ // a body belongs to the reference nearest it
234
+ for (const { el: b, box } of bodies) {
235
+ let best: SchematicPart | undefined;
236
+ let bestD = Infinity;
237
+ for (const p of parts) {
238
+ const dx = Math.max(box[0] - p.at[0], 0, p.at[0] - (box[0] + box[2]));
239
+ const dy = Math.max(box[1] - p.at[1], 0, p.at[1] - (box[1] + box[3]));
240
+ const d = Math.hypot(dx, dy);
241
+ if (d < bestD) [best, bestD] = [p, d];
242
+ }
243
+ if (best && bestD < 8 && !best.box) {
244
+ best.box = box;
245
+ best.els.push(b);
246
+ }
247
+ }
248
+ const netAt = (p: Pt, reach: number): string | undefined => {
249
+ let best: string | undefined;
250
+ let bestD = reach;
251
+ for (const i of grid.near(p, reach)) {
252
+ const d = distance(p, segs[i]);
253
+ if (d <= bestD) [best, bestD] = [segmentNet[i], d];
254
+ }
255
+ labels.forEach((l, i) => {
256
+ const net = labelNet[i];
257
+ if (!net) return;
258
+ const d = distance(p, { a: l.at, b: l.end });
259
+ if (d <= bestD) [best, bestD] = [net, d];
260
+ });
261
+ return best;
262
+ };
263
+ return { nets, netOf, parts, netAt };
264
+ }
265
+
266
+ /** The part at a drawing point: the one whose body holds it, else the reference nearest it within `reach`. */
267
+ export function partAt(parts: SchematicPart[], p: Pt, reach = 4): SchematicPart | undefined {
268
+ const inside = parts.filter((q) => q.box && p[0] >= q.box[0] && p[0] <= q.box[0] + q.box[2] && p[1] >= q.box[1] && p[1] <= q.box[1] + q.box[3]);
269
+ if (inside.length) return inside.sort((a, b) => a.box![2] * a.box![3] - b.box![2] * b.box![3])[0];
270
+ let best: SchematicPart | undefined;
271
+ let bestD = reach;
272
+ for (const q of parts) {
273
+ const d = Math.hypot(q.at[0] - p[0], q.at[1] - p[1]);
274
+ if (d < bestD) [best, bestD] = [q, d];
275
+ }
276
+ return best;
277
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The colours circuit tools write into their standard-output drawings and the
3
+ * role each plays, and what each role is drawn with in the shell's theme.
4
+ *
5
+ * The drawings are found by file name (schematic.svg, pcb-top.svg,
6
+ * pcb-bottom.svg; docs/specs/tool-integrations.md §8), never by the tool. The
7
+ * palettes below know the colours of the drawings the stack's circuit
8
+ * provider writes: tscircuit's circuit-to-svg defaults, and KiCad's default
9
+ * colours for a locked board's export (`kicad-cli sch|pcb export svg`); a
10
+ * drawing in other colours keeps them.
11
+ */
12
+ import type { Palette } from "./svg.js";
13
+
14
+ const map = (entries: Record<string, string>): Palette => new Map(Object.entries(entries));
15
+
16
+ /** Schematic roles: paper, ink (symbols, outlines), body (part fill), wire, pin numbers, labels. */
17
+ export const SCHEMATIC_PALETTE = map({
18
+ "#f5f1ed": "paper",
19
+ "#ffffff": "paper",
20
+ "#840000": "ink",
21
+ "#ffffc2": "body",
22
+ "#009600": "wire",
23
+ "#a90000": "pin",
24
+ "#006464": "label",
25
+ "#0f0f0f": "net-text",
26
+ "#ffffff/0.6": "net-fill",
27
+ "#ff0000": "hover",
28
+ // KiCad (Eeschema): the sheet, labels and their text, no-connect marks and buses
29
+ "#f5f4ef": "paper",
30
+ "#000000": "net-text",
31
+ "#000084": "ink",
32
+ "#c2c2c2": "label",
33
+ });
34
+
35
+ /** PCB roles: the page, copper on each side, silkscreen, drills, the board's edge, keepouts. */
36
+ export const PCB_PALETTE = map({
37
+ "#000000": "paper",
38
+ "#c83434": "copper-top",
39
+ "#4d7fc4": "copper-bottom",
40
+ "#ff26e2": "drill",
41
+ "#f2eda1": "silk",
42
+ "#5da9e9": "silk",
43
+ "#ffffff": "edge",
44
+ "#ffffff/0.5": "edge",
45
+ "#ff6b6b": "keepout",
46
+ "#ff6b6b/0.2": "keepout-fill",
47
+ // KiCad (Pcbnew): the mask's openings over the pads on each side, the bottom silkscreen, the edge cuts
48
+ "#d864ff": "pad",
49
+ "#02ffee": "pad",
50
+ "#e8b2a7": "silk",
51
+ "#d0d2cd": "edge",
52
+ });
53
+
54
+ /**
55
+ * The roles in theme tokens. Copper and drills are data colours: the same
56
+ * family in every theme, mixed toward the canvas so they sit on it; the
57
+ * page, ink, labels and edges are the theme's own. High contrast draws every
58
+ * line in the foreground and keeps fills out of the way.
59
+ */
60
+ export const DRAWING_CSS = `
61
+ .pb-board-drawing {
62
+ --pb-d-paper: var(--uhd-canvas);
63
+ --pb-d-ink: color-mix(in oklab, var(--uhd-text) 88%, var(--uhd-canvas));
64
+ --pb-d-body: var(--uhd-node);
65
+ --pb-d-wire: var(--uhd-domain-software);
66
+ /* pin names and numbers: quieter than the ink, but opaque and readable on every canvas (a theme's faint text may be translucent) */
67
+ --pb-d-pin: color-mix(in oklab, var(--uhd-text) 70%, var(--uhd-canvas));
68
+ --pb-d-label: var(--uhd-text-muted);
69
+ --pb-d-net-text: var(--uhd-text);
70
+ --pb-d-net-fill: color-mix(in oklab, var(--uhd-surface) 85%, transparent);
71
+ --pb-d-hover: var(--uhd-highlight);
72
+ --pb-d-board: color-mix(in oklab, var(--uhd-text) 7%, var(--uhd-canvas));
73
+ --pb-d-copper-top: color-mix(in oklab, #c94a3e 85%, var(--uhd-canvas));
74
+ --pb-d-copper-bottom: color-mix(in oklab, #4d7fc4 82%, var(--uhd-canvas));
75
+ --pb-d-drill: color-mix(in oklab, var(--uhd-text) 30%, var(--uhd-canvas));
76
+ --pb-d-silk: color-mix(in oklab, var(--uhd-text) 92%, var(--uhd-canvas));
77
+ --pb-d-edge: var(--uhd-border-strong);
78
+ --pb-d-keepout: var(--uhd-severity-error);
79
+ --pb-d-keepout-fill: color-mix(in oklab, var(--uhd-severity-error) 14%, transparent);
80
+ --pb-d-select: var(--uhd-accent);
81
+ /* the routed board (CircuitPcbView): inner copper, the far side's silkscreen, courtyards, the fabrication drawing */
82
+ --pb-d-copper-inner-0: color-mix(in oklab, #d1a23a 80%, var(--uhd-canvas));
83
+ --pb-d-copper-inner-1: color-mix(in oklab, #4fa66a 78%, var(--uhd-canvas));
84
+ --pb-d-copper-inner-2: color-mix(in oklab, #9a6cc9 78%, var(--uhd-canvas));
85
+ --pb-d-copper-inner-3: color-mix(in oklab, #3aa7a7 78%, var(--uhd-canvas));
86
+ --pb-d-silk-bottom: color-mix(in oklab, var(--uhd-text) 55%, var(--uhd-canvas));
87
+ --pb-d-courtyard: color-mix(in oklab, var(--uhd-text-muted) 80%, var(--uhd-canvas));
88
+ --pb-d-fab: color-mix(in oklab, var(--uhd-domain-mechanical, var(--uhd-text-muted)) 75%, var(--uhd-canvas));
89
+ /* a pad the solder mask leaves open (KiCad's export draws the openings over the copper): the finish */
90
+ --pb-d-pad: color-mix(in oklab, #d9a441 82%, var(--uhd-canvas));
91
+ }
92
+ /* the board check's marks on the routed board: a fixed size on screen at any zoom (--pb-px: drawing units per pixel) */
93
+ .pb-pcb .pb-finding-ring { r: calc(var(--pb-px, 0.05) * 6px); opacity: 0.9; }
94
+ .pb-pcb .pb-finding-dot { r: calc(var(--pb-px, 0.05) * 2px); }
95
+ .pb-pcb [data-severity="warning"] .pb-finding-ring { r: calc(var(--pb-px, 0.05) * 4.5px); opacity: 0.75; }
96
+ .pb-pcb [data-severity="info"] .pb-finding-ring { r: calc(var(--pb-px, 0.05) * 3.5px); opacity: 0.6; }
97
+ .pb-pcb .pb-pcb-layer[data-layer="edge"] { color: var(--pb-d-board); }
98
+ .pb-pcb .pb-pcb-layer[data-layer="edge"] > * { fill: var(--pb-d-board); stroke: var(--pb-d-edge); stroke-width: 0.12px; vector-effect: non-scaling-stroke; }
99
+ .pb-pcb .pb-pcb-copper > * { opacity: 0.9; }
100
+ .pb-pcb .pb-pcb-copper .pb-pour { opacity: 0.2; }
101
+ .pb-pcb .pb-pcb-layer[data-layer="drill"] { color: var(--pb-d-paper); }
102
+ .pb-pcb [data-net], .pb-pcb [data-ref] { cursor: pointer; }
103
+ .pb-pcb .pb-pcb-findings { pointer-events: none; }
104
+ .pb-drawing { background: var(--pb-d-paper); }
105
+ .pb-drawing svg { display: block; overflow: visible; }
106
+ .pb-drawing text { font-family: var(--uhd-font-ui); }
107
+ .pb-drawing .pcb-board { fill: var(--pb-d-board); }
108
+ .pb-drawing .schematic-boundary, .pb-drawing .pcb-boundary { stroke: var(--pb-d-edge); }
109
+ /* hit areas the tool draws invisible stay invisible */
110
+ .pb-drawing .trace-invisible-hover-outline, .pb-drawing .schematic-port-hover { opacity: 0; }
111
+ /* what the pointer is over, and what is selected */
112
+ .pb-drawing [data-pb-hot] { cursor: pointer; }
113
+ .pb-drawing .pb-net-on :is(path, line) { stroke: var(--pb-d-hover) !important; stroke-width: 0.9px; }
114
+ .pb-drawing .pb-net-on.sch-trace-junction, .pb-drawing .pb-net-on circle { fill: var(--pb-d-hover) !important; }
115
+ .pb-drawing .pb-pad-on { fill: var(--pb-d-hover) !important; stroke: var(--pb-d-hover) !important; }
116
+ .pb-drawing .pb-part-hover :is(.sch-component-body, .sch-component-symbol-path) { stroke: var(--pb-d-hover) !important; }
117
+ .pb-drawing .pb-part-on :is(.sch-component-body, .sch-component-symbol-path, .component-pin) { stroke: var(--pb-d-select) !important; stroke-width: 0.9px; }
118
+ .pb-drawing .pb-part-on .sch-component-body { fill: color-mix(in oklab, var(--pb-d-select) 14%, var(--pb-d-body)) !important; }
119
+ .pb-drawing .pb-part-on text { fill: var(--pb-d-select) !important; }
120
+ .pb-drawing .pb-pad-part { fill: var(--pb-d-select) !important; }
121
+ /* KiCad's drawings: lines never thinner than a pixel at any zoom (its strokes are a tenth of a millimetre), wires as wide as the symbols' */
122
+ .pb-kicad .pb-drawing svg :is(path, line, polyline, polygon, circle, rect) { stroke-width: max(var(--pb-kicad-w, 0.1524px), calc(var(--pb-px, 0.05) * 0.9px)); }
123
+ .pb-kicad .pb-drawing [data-pb-stroke="wire"], .pb-kicad .pb-drawing [data-pb-stroke="wire"] :is(path, line, polyline) { --pb-kicad-w: 0.254px; }
124
+ .pb-kicad .pb-drawing [data-pb-page] { stroke: none; }
125
+ /* a KiCad net lit end to end (its wires and labels), a part picked (its reference, value and body) */
126
+ .pb-drawing .pb-kicad-net, .pb-drawing .pb-kicad-net path { stroke: var(--pb-d-hover) !important; stroke-width: max(0.3px, calc(var(--pb-px, 0.05) * 2.5px)) !important; }
127
+ .pb-drawing .pb-kicad-part-hover, .pb-drawing .pb-kicad-part-hover path { stroke: var(--pb-d-hover) !important; }
128
+ .pb-drawing .pb-kicad-part, .pb-drawing .pb-kicad-part path { stroke: var(--pb-d-select) !important; }
129
+ .pb-drawing [data-pb-fill="body"].pb-kicad-part { fill: color-mix(in oklab, var(--pb-d-select) 18%, var(--pb-d-body)) !important; stroke-width: max(0.25px, calc(var(--pb-px, 0.05) * 2px)) !important; }
130
+ /* KiCad's board export fills a pour as many polygons side by side: a hairline of the same copper closes the seams between them */
131
+ .pb-kicad-pcb .pb-drawing [data-pb-fill^="copper"]:not([data-pb-stroke]) { stroke: var(--pb-d-copper-top) !important; stroke-width: 0.02px !important; }
132
+ .pb-kicad-pcb .pb-drawing [data-pb-fill="copper-bottom"]:not([data-pb-stroke]) { stroke: var(--pb-d-copper-bottom) !important; }
133
+ [data-uhd-scheme^="high-contrast"] .pb-board-drawing {
134
+ --pb-d-ink: var(--uhd-text);
135
+ --pb-d-body: var(--uhd-canvas);
136
+ --pb-d-wire: var(--uhd-text);
137
+ --pb-d-pin: var(--uhd-text);
138
+ --pb-d-label: var(--uhd-text);
139
+ --pb-d-net-fill: var(--uhd-canvas);
140
+ --pb-d-board: var(--uhd-canvas);
141
+ --pb-d-silk: var(--uhd-text);
142
+ --pb-d-edge: var(--uhd-contrast-border);
143
+ --pb-d-hover: var(--uhd-contrast-active-border);
144
+ --pb-d-select: var(--uhd-contrast-active-border);
145
+ --pb-d-silk-bottom: var(--uhd-text);
146
+ --pb-d-courtyard: var(--uhd-text);
147
+ --pb-d-fab: var(--uhd-text);
148
+ --pb-d-pad: var(--uhd-text);
149
+ }
150
+ `;