@protoboardai/ext-tscircuit 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.
- package/LICENSE +202 -0
- package/NOTICE +59 -0
- package/README.md +493 -2
- package/evals/announce-pcb-fallback/case.ts +67 -0
- package/evals/catch-swapped-nets/case.ts +69 -0
- package/evals/generate-imu-board/case.ts +60 -0
- package/evals/locked-board-fix-upstream/case.ts +70 -0
- package/lib/checks.ts +464 -0
- package/lib/config.ts +384 -0
- package/lib/fingerprint.ts +144 -0
- package/lib/footprints.ts +370 -0
- package/lib/kicad/cli.ts +93 -0
- package/lib/kicad/drc.ts +95 -0
- package/lib/kicad/fields.ts +65 -0
- package/lib/kicad/handoff.ts +112 -0
- package/lib/kicad/netlists.ts +311 -0
- package/lib/kicad/probe.cjs +21 -0
- package/lib/kicad/project.ts +594 -0
- package/lib/kicad/refdes.ts +130 -0
- package/lib/kicad/schematic.ts +624 -0
- package/lib/kicad/sexpr.ts +117 -0
- package/lib/land-geometry.ts +177 -0
- package/lib/land-patterns.ts +706 -0
- package/lib/layout.ts +236 -0
- package/lib/models.ts +284 -0
- package/lib/native.ts +199 -0
- package/lib/outputs.ts +383 -0
- package/lib/paste.ts +69 -0
- package/lib/probe.cjs +31 -0
- package/lib/runtime.ts +185 -0
- package/lib/silk.ts +141 -0
- package/lib/source.ts +472 -0
- package/lib/stepfile.ts +210 -0
- package/lib/suppliers.ts +39 -0
- package/ops/circuit.ts +270 -0
- package/ops/kicad.ts +636 -0
- package/ops/package.ts +156 -0
- package/package.json +81 -4
- package/protoboard-extension.json +313 -0
- package/skills/circuit-board/SKILL.md +494 -0
- package/skills/circuit-board/references/pcb-workflow.md +87 -0
- package/skills/kicad-cli/SKILL.md +210 -0
- package/skills/kicad-layout-review/SKILL.md +275 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The KiCad schematic of a board, written by the provider (PB-1032), not
|
|
3
|
+
* converted from tscircuit's schematic: a person reads it, so it is laid out
|
|
4
|
+
* the way one would draw it.
|
|
5
|
+
*
|
|
6
|
+
* symbols one per part (and pin set), pins named as the model names them
|
|
7
|
+
* (`IN+`, `D-`, `A`/`K`; a long note in brackets is dropped, and two
|
|
8
|
+
* different pins of one name become `GND`, `GND_2`), electrical types
|
|
9
|
+
* from the UHD leaf types (power in/out, input, output, open
|
|
10
|
+
* collector, bidirectional, passive), every pin on the 1.27 mm grid
|
|
11
|
+
* blocks one labelled block per sub-module (UHD_Group, the instance path)
|
|
12
|
+
* and one for the board's own parts; in a block each IC comes first
|
|
13
|
+
* with the passives it connects to in columns beside it
|
|
14
|
+
* nets a short stub and a global label at every connected pin, named by
|
|
15
|
+
* the board's net (no long wires, nothing through a symbol body); a
|
|
16
|
+
* pin on no net gets a no-connect flag
|
|
17
|
+
* flags a PWR_FLAG on every net with a power input and no power output,
|
|
18
|
+
* so ERC checks what matters
|
|
19
|
+
* sheet the smallest ISO sheet the blocks fit on (A4 to A0), else a user size
|
|
20
|
+
*
|
|
21
|
+
* The connections are the board's own (the pads' nets in the exported
|
|
22
|
+
* .kicad_pcb), so Update PCB from Schematic changes nothing.
|
|
23
|
+
*/
|
|
24
|
+
import type { BoardComponent, BoardNetlist, BoardPinKind } from "@protoboardai/core";
|
|
25
|
+
import type { ProtocolDef } from "@deltarobotics/uhd";
|
|
26
|
+
import { list, str, type List, type Node } from "./sexpr.ts";
|
|
27
|
+
|
|
28
|
+
/** KiCad's electrical pin types (those used here). */
|
|
29
|
+
export type KicadPinType = "input" | "output" | "bidirectional" | "open_collector" | "passive" | "power_in" | "power_out" | "no_connect";
|
|
30
|
+
|
|
31
|
+
/** What a leaf interface is to KiCad: its BoardPin kind refined by its protocols' roles and capabilities. */
|
|
32
|
+
export function kicadPinType(kind: BoardPinKind, leaf?: { protocols?: Pick<ProtocolDef, "type" | "roles">[]; capabilities?: string[] }): KicadPinType {
|
|
33
|
+
if (kind === "ground" || kind === "power_in") return "power_in";
|
|
34
|
+
if (kind === "power_out") return "power_out";
|
|
35
|
+
if (kind === "passive") return "passive";
|
|
36
|
+
if (kind === "nc") return "no_connect";
|
|
37
|
+
const roles = new Set((leaf?.protocols ?? []).filter((p) => p.type !== "passive").flatMap((p) => p.roles ?? []));
|
|
38
|
+
const caps = leaf?.capabilities ?? [];
|
|
39
|
+
const input = roles.has("input") || roles.has("sink");
|
|
40
|
+
const output = roles.has("output") || roles.has("source");
|
|
41
|
+
if (roles.has("bidirectional") || (input && output)) return "bidirectional";
|
|
42
|
+
if (output) return caps.some((c) => c === "open_drain" || c === "open_collector") ? "open_collector" : "output";
|
|
43
|
+
if (input) return "input";
|
|
44
|
+
return "passive";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SchematicInput {
|
|
48
|
+
board: string;
|
|
49
|
+
/** The project's library name (`<lib>:<symbol>`). */
|
|
50
|
+
lib: string;
|
|
51
|
+
netlist: BoardNetlist;
|
|
52
|
+
/** instance → its pads: pad number → UHD leaf interface (the build's name map; tscircuit's pad numbers are KiCad's). */
|
|
53
|
+
pads: Map<string, Map<string, string>>;
|
|
54
|
+
/** instance → designator */
|
|
55
|
+
refdes: Record<string, string>;
|
|
56
|
+
/** instance → fields as name/value pairs, in order (Value first). */
|
|
57
|
+
fields: Record<string, [string, string][]>;
|
|
58
|
+
/** instance → footprint library id */
|
|
59
|
+
footprint: (instance: string) => string;
|
|
60
|
+
/** designator → pad → net (the exported board's copper). */
|
|
61
|
+
padNets: Map<string, Map<string, string>>;
|
|
62
|
+
/** instance → leaf interface → KiCad type (absent: from the pin's kind). */
|
|
63
|
+
pinTypes?: Record<string, Record<string, KicadPinType>>;
|
|
64
|
+
/** instance → symbol UUID */
|
|
65
|
+
symbolUuid: Record<string, string>;
|
|
66
|
+
uuid: (key: string) => string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Schematic {
|
|
70
|
+
sch: List;
|
|
71
|
+
/** The library symbols, named `<lib>:<name>` (the project's .kicad_sym holds them without the prefix). */
|
|
72
|
+
libSymbols: List[];
|
|
73
|
+
/** instance → its symbol's library id */
|
|
74
|
+
symbolOf: Record<string, string>;
|
|
75
|
+
/** library id → pad → pin name */
|
|
76
|
+
pinNames: Map<string, Map<string, string>>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Geometry
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const G = 2.54; // the grid symbols and pins sit on (a multiple of KiCad's 1.27 mm)
|
|
84
|
+
const FONT = 1.27;
|
|
85
|
+
/** Width of a string in KiCad's stroke font at 1.27 mm (a little generous). */
|
|
86
|
+
const textW = (s: string, size = FONT) => s.length * size * 0.92;
|
|
87
|
+
const up = (v: number) => Math.ceil(v / G - 1e-9) * G;
|
|
88
|
+
const r = (v: number) => String(Math.round(v * 1e4) / 1e4 + 0);
|
|
89
|
+
const at = (x: number, y: number, a = 0) => list("at", r(x), r(y), r(a));
|
|
90
|
+
const effects = (size = FONT, justify?: string[], hide = false) => {
|
|
91
|
+
const e = list("effects", list("font", list("size", r(size), r(size))));
|
|
92
|
+
if (justify?.length) e.items.push(list("justify", ...justify));
|
|
93
|
+
if (hide) e.items.push(list("hide", "yes"));
|
|
94
|
+
return e;
|
|
95
|
+
};
|
|
96
|
+
const stroke = (width = 0.254, type = "default") => list("stroke", list("width", r(width)), list("type", type));
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Symbols
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
interface PinSpec {
|
|
103
|
+
pad: string;
|
|
104
|
+
name: string;
|
|
105
|
+
type: KicadPinType;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface SymbolSpec {
|
|
109
|
+
/** The library item name. */
|
|
110
|
+
name: string;
|
|
111
|
+
prefix: string;
|
|
112
|
+
kind: "two" | "box";
|
|
113
|
+
passive?: BoardComponent["kind"];
|
|
114
|
+
pins: PinSpec[];
|
|
115
|
+
/** box: pins on each side, top to bottom */
|
|
116
|
+
left: PinSpec[];
|
|
117
|
+
right: PinSpec[];
|
|
118
|
+
/** box: half the body's width; both: rows of pins */
|
|
119
|
+
hw: number;
|
|
120
|
+
rows: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const PASSIVES = new Set(["resistor", "capacitor", "inductor", "ferrite_bead"]);
|
|
124
|
+
|
|
125
|
+
/** A pin name KiCad shows as written: the model's name, a note in brackets dropped, spaces joined. */
|
|
126
|
+
export function pinName(name: string): string {
|
|
127
|
+
let n = name.trim();
|
|
128
|
+
const bracket = n.indexOf(" (");
|
|
129
|
+
if (bracket > 0) n = n.slice(0, bracket);
|
|
130
|
+
n = n.replace(/\s+/g, "_");
|
|
131
|
+
// KiCad's text markup (~{…}, _{…}, ^{…}) and a lone ~ (no name) are not the model's
|
|
132
|
+
if (/^~/.test(n) || /[~^_]\{/.test(n)) n = n.replace(/([~^_])\{/g, "$1(").replace(/^~/, "-");
|
|
133
|
+
return n || "~";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Side of a box symbol a pin goes on: supplies and inputs left, outputs right, ground at the bottom of the left. */
|
|
137
|
+
function sides(pins: PinSpec[], kindOf: (p: PinSpec) => BoardPinKind): { left: PinSpec[]; right: PinSpec[] } {
|
|
138
|
+
const supply = pins.filter((p) => p.type === "power_in" && kindOf(p) !== "ground");
|
|
139
|
+
const out = pins.filter((p) => p.type === "power_out");
|
|
140
|
+
const ins = pins.filter((p) => p.type === "input");
|
|
141
|
+
const outs = pins.filter((p) => p.type === "output" || p.type === "open_collector");
|
|
142
|
+
const ground = pins.filter((p) => kindOf(p) === "ground");
|
|
143
|
+
const rest = pins.filter((p) => ![supply, out, ins, outs, ground].some((s) => s.includes(p)));
|
|
144
|
+
const left = [...supply, ...ins];
|
|
145
|
+
const right = [...out, ...outs];
|
|
146
|
+
// the rest (bidirectional, passive, no-connect) fill the shorter side, in pad order
|
|
147
|
+
for (const p of rest) (left.length + ground.length <= right.length ? left : right).push(p);
|
|
148
|
+
left.push(...ground);
|
|
149
|
+
// a lopsided symbol is hard to read: move pins from the longer side's end until the two are within two
|
|
150
|
+
while (left.length - right.length > 2 && left.length > ground.length) right.push(left.splice(left.length - ground.length - 1, 1)[0]);
|
|
151
|
+
while (right.length - left.length > 2) left.splice(left.length - ground.length, 0, right.pop()!);
|
|
152
|
+
return { left, right };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function symbolSpec(c: BoardComponent, pads: Map<string, string>, typeOf: (pad: string, iface: string) => KicadPinType, netOf: (pad: string) => string | undefined, prefix: string): Omit<SymbolSpec, "name"> {
|
|
156
|
+
const byIface = new Map(c.pins.map((p) => [p.interface, p]));
|
|
157
|
+
const order = [...pads.entries()].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
|
158
|
+
// one name per interface, as the model names it; pins of one name stay one name while they are on one net
|
|
159
|
+
// (a module's ground pads), and one on another net is told apart visibly (GND, GND_2)
|
|
160
|
+
const nameOf = new Map<string, string>();
|
|
161
|
+
const firstNet = new Map<string, string | undefined>(); // shown name → the net of its first pin
|
|
162
|
+
const count = new Map<string, number>();
|
|
163
|
+
for (const [pad, iface] of order) {
|
|
164
|
+
if (nameOf.has(iface)) continue;
|
|
165
|
+
const base = pinName(byIface.get(iface)?.name ?? iface);
|
|
166
|
+
const net = netOf(pad);
|
|
167
|
+
if (!count.has(base) || firstNet.get(base) === net) {
|
|
168
|
+
if (!count.has(base)) (count.set(base, 1), firstNet.set(base, net));
|
|
169
|
+
nameOf.set(iface, base);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let n = count.get(base)! + 1;
|
|
173
|
+
while (count.has(`${base}_${n}`)) n++;
|
|
174
|
+
count.set(base, n);
|
|
175
|
+
count.set(`${base}_${n}`, 1);
|
|
176
|
+
firstNet.set(`${base}_${n}`, net);
|
|
177
|
+
nameOf.set(iface, `${base}_${n}`);
|
|
178
|
+
}
|
|
179
|
+
const pins: PinSpec[] = order.map(([pad, iface]) => ({ pad, name: nameOf.get(iface)!, type: PASSIVES.has(c.kind) ? "passive" : typeOf(pad, iface) }));
|
|
180
|
+
const kindByPad = new Map(order.map(([pad, iface]) => [pad, byIface.get(iface)?.kind ?? "passive"]));
|
|
181
|
+
if (PASSIVES.has(c.kind) && pins.length === 2) return { prefix, kind: "two", passive: c.kind, pins, left: [pins[0]], right: [pins[1]], hw: G, rows: 1 };
|
|
182
|
+
const { left, right } = sides(pins, (p) => kindByPad.get(p.pad)!);
|
|
183
|
+
const rows = Math.max(left.length, right.length, 1);
|
|
184
|
+
const nameW = Math.max(0, ...left.map((p) => textW(p.name))) + Math.max(0, ...right.map((p) => textW(p.name)));
|
|
185
|
+
const hw = Math.max(2 * G, up((nameW + 2 * G) / 2));
|
|
186
|
+
return { prefix, kind: "box", pins, left, right, hw, rows };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The pin's connection point in library coordinates (Y up) and its angle. */
|
|
190
|
+
function pinPlaces(s: SymbolSpec): Map<string, { x: number; y: number; angle: number }> {
|
|
191
|
+
const out = new Map<string, { x: number; y: number; angle: number }>();
|
|
192
|
+
if (s.kind === "two") {
|
|
193
|
+
out.set(s.left[0].pad, { x: -2 * G, y: 0, angle: 0 });
|
|
194
|
+
out.set(s.right[0].pad, { x: 2 * G, y: 0, angle: 180 });
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
s.left.forEach((p, i) => out.set(p.pad, { x: -(s.hw + G), y: -i * G, angle: 0 }));
|
|
198
|
+
s.right.forEach((p, i) => out.set(p.pad, { x: s.hw + G, y: -i * G, angle: 180 }));
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function libSymbol(s: SymbolSpec, lib: string): List {
|
|
203
|
+
const name = s.name;
|
|
204
|
+
const props = [
|
|
205
|
+
list("property", str("Reference"), str(s.prefix), at(0, s.kind === "two" ? G : 2 * G), effects()),
|
|
206
|
+
list("property", str("Value"), str(name), at(0, s.kind === "two" ? -G : -(s.rows * G + G)), effects()),
|
|
207
|
+
list("property", str("Footprint"), str(""), at(0, 0), effects(FONT, undefined, true)),
|
|
208
|
+
list("property", str("Datasheet"), str(""), at(0, 0), effects(FONT, undefined, true)),
|
|
209
|
+
list("property", str("Description"), str(""), at(0, 0), effects(FONT, undefined, true)),
|
|
210
|
+
];
|
|
211
|
+
const body: Node[] = [];
|
|
212
|
+
if (s.kind === "two") {
|
|
213
|
+
if (s.passive === "capacitor") {
|
|
214
|
+
body.push(
|
|
215
|
+
list("polyline", list("pts", list("xy", "-0.635", "-1.524"), list("xy", "-0.635", "1.524")), stroke(0.3048), list("fill", list("type", "none"))),
|
|
216
|
+
list("polyline", list("pts", list("xy", "0.635", "-1.524"), list("xy", "0.635", "1.524")), stroke(0.3048), list("fill", list("type", "none"))),
|
|
217
|
+
list("polyline", list("pts", list("xy", "-2.54", "0"), list("xy", "-0.635", "0")), stroke(0.254), list("fill", list("type", "none"))),
|
|
218
|
+
list("polyline", list("pts", list("xy", "0.635", "0"), list("xy", "2.54", "0")), stroke(0.254), list("fill", list("type", "none"))),
|
|
219
|
+
);
|
|
220
|
+
} else if (s.passive === "inductor") {
|
|
221
|
+
for (let i = 0; i < 4; i++) {
|
|
222
|
+
const x0 = -2.54 + i * 1.27;
|
|
223
|
+
body.push(list("arc", list("start", r(x0), "0"), list("mid", r(x0 + 0.635), "0.635"), list("end", r(x0 + 1.27), "0"), stroke(0.254), list("fill", list("type", "none"))));
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
body.push(list("rectangle", list("start", "-2.54", "1.016"), list("end", "2.54", "-1.016"), stroke(0.254), list("fill", list("type", s.passive === "ferrite_bead" ? "outline" : "none"))));
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
body.push(list("rectangle", list("start", r(-s.hw), r(G)), list("end", r(s.hw), r(-(s.rows * G))), stroke(0.254), list("fill", list("type", "background"))));
|
|
230
|
+
}
|
|
231
|
+
const places = pinPlaces(s);
|
|
232
|
+
const pins = s.pins.map((p) => {
|
|
233
|
+
const pl = places.get(p.pad)!;
|
|
234
|
+
return list("pin", p.type, "line", at(pl.x, pl.y, pl.angle), list("length", r(G)), list("name", str(p.name), effects()), list("number", str(p.pad), effects()));
|
|
235
|
+
});
|
|
236
|
+
return list(
|
|
237
|
+
"symbol",
|
|
238
|
+
str(`${lib}:${name}`),
|
|
239
|
+
...(s.kind === "two" ? [list("pin_numbers", list("hide", "yes")), list("pin_names", list("offset", "0"), list("hide", "yes"))] : [list("pin_names", list("offset", "0.762"))]),
|
|
240
|
+
list("exclude_from_sim", "no"),
|
|
241
|
+
list("in_bom", "yes"),
|
|
242
|
+
list("on_board", "yes"),
|
|
243
|
+
...props,
|
|
244
|
+
list("symbol", str(`${name}_0_1`), ...body),
|
|
245
|
+
list("symbol", str(`${name}_1_1`), ...pins),
|
|
246
|
+
list("embedded_fonts", "no"),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** KiCad's PWR_FLAG, in the project's library. */
|
|
251
|
+
function pwrFlag(lib: string): List {
|
|
252
|
+
return list(
|
|
253
|
+
"symbol",
|
|
254
|
+
str(`${lib}:PWR_FLAG`),
|
|
255
|
+
list("power"),
|
|
256
|
+
list("pin_numbers", list("hide", "yes")),
|
|
257
|
+
list("pin_names", list("offset", "0"), list("hide", "yes")),
|
|
258
|
+
list("exclude_from_sim", "no"),
|
|
259
|
+
list("in_bom", "yes"),
|
|
260
|
+
list("on_board", "yes"),
|
|
261
|
+
list("property", str("Reference"), str("#FLG"), at(0, 1.905), effects(FONT, undefined, true)),
|
|
262
|
+
list("property", str("Value"), str("PWR_FLAG"), at(0, 3.81), effects()),
|
|
263
|
+
list("property", str("Footprint"), str(""), at(0, 0), effects(FONT, undefined, true)),
|
|
264
|
+
list("property", str("Datasheet"), str("~"), at(0, 0), effects(FONT, undefined, true)),
|
|
265
|
+
list("property", str("Description"), str("Special symbol for telling ERC where power comes from"), at(0, 0), effects(FONT, undefined, true)),
|
|
266
|
+
list("symbol", str("PWR_FLAG_0_0"), list("pin", "power_out", "line", at(0, 0, 90), list("length", "0"), list("name", str("~"), effects()), list("number", str("1"), effects()))),
|
|
267
|
+
list("symbol", str("PWR_FLAG_0_1"), list("polyline", list("pts", list("xy", "0", "0"), list("xy", "0", "1.27"), list("xy", "-1.016", "1.905"), list("xy", "0", "2.54"), list("xy", "1.016", "1.905"), list("xy", "0", "1.27")), stroke(0), list("fill", list("type", "none")))),
|
|
268
|
+
list("embedded_fonts", "no"),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Layout
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
/** A label's extent from its pin: the stub and the label's own width. */
|
|
277
|
+
const STUB = 2 * G;
|
|
278
|
+
const labelW = (net: string) => STUB + textW(net) + 2.6;
|
|
279
|
+
|
|
280
|
+
interface Placed {
|
|
281
|
+
inst: string;
|
|
282
|
+
spec: SymbolSpec;
|
|
283
|
+
/** Symbol origin, relative to its cluster (then its block, then the sheet). */
|
|
284
|
+
x: number;
|
|
285
|
+
y: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface Box {
|
|
289
|
+
w: number;
|
|
290
|
+
h: number;
|
|
291
|
+
items: Placed[];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Extents of a symbol with its labels around its origin: left, right, top, bottom (sheet orientation, positive outward). */
|
|
295
|
+
function extent(spec: SymbolSpec, nets: Map<string, string> | undefined): { l: number; r: number; t: number; b: number } {
|
|
296
|
+
const lw = Math.max(0, ...spec.left.map((p) => (nets?.get(p.pad) ? labelW(nets.get(p.pad)!) : G)));
|
|
297
|
+
const rw = Math.max(0, ...spec.right.map((p) => (nets?.get(p.pad) ? labelW(nets.get(p.pad)!) : G)));
|
|
298
|
+
if (spec.kind === "two") return { l: 2 * G + lw, r: 2 * G + rw, t: 2 * G, b: 2 * G };
|
|
299
|
+
return { l: spec.hw + G + lw, r: spec.hw + G + rw, t: 3 * G, b: spec.rows * G + 2 * G };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Lay boxes in rows no wider than `width` (each at least its own width), with `gap` between them. */
|
|
303
|
+
function shelf<T extends { w: number; h: number }>(boxes: T[], width: number, gap: number): { box: T; x: number; y: number }[] {
|
|
304
|
+
const out: { box: T; x: number; y: number }[] = [];
|
|
305
|
+
let x = 0;
|
|
306
|
+
let y = 0;
|
|
307
|
+
let rowH = 0;
|
|
308
|
+
for (const b of boxes) {
|
|
309
|
+
if (x > 0 && x + b.w > width) {
|
|
310
|
+
x = 0;
|
|
311
|
+
y += rowH + gap;
|
|
312
|
+
rowH = 0;
|
|
313
|
+
}
|
|
314
|
+
out.push({ box: b, x, y });
|
|
315
|
+
x += up(b.w) + gap;
|
|
316
|
+
rowH = Math.max(rowH, up(b.h));
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const PAPERS: [string, number, number][] = [
|
|
322
|
+
["A4", 297, 210],
|
|
323
|
+
["A3", 420, 297],
|
|
324
|
+
["A2", 594, 420],
|
|
325
|
+
["A1", 841, 594],
|
|
326
|
+
["A0", 1189, 841],
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
export function buildSchematic(input: SchematicInput): Schematic {
|
|
330
|
+
const { board, lib, netlist, uuid } = input;
|
|
331
|
+
const comps = netlist.components.filter((c) => input.refdes[c.instance] && input.pads.has(c.instance));
|
|
332
|
+
const netsOf = (inst: string) => input.padNets.get(input.refdes[inst]);
|
|
333
|
+
|
|
334
|
+
// ---- symbols: one per part and pin set
|
|
335
|
+
const specs = new Map<string, SymbolSpec>(); // instance → spec
|
|
336
|
+
const byKey = new Map<string, SymbolSpec>();
|
|
337
|
+
const names = new Set<string>(["PWR_FLAG"]);
|
|
338
|
+
// electrical types: the leaf's, then made to say what ERC should check
|
|
339
|
+
const baseType = (c: BoardComponent, pad: string, iface: string): KicadPinType => {
|
|
340
|
+
const kind = c.pins.find((p) => p.interface === iface)?.kind ?? "passive";
|
|
341
|
+
let type = input.pinTypes?.[c.instance]?.[iface] ?? kicadPinType(kind);
|
|
342
|
+
// a no-connect pin that is wired (tied to ground on purpose) is passive, or ERC calls the net an error
|
|
343
|
+
if (type === "no_connect" && netsOf(c.instance)?.get(pad)) type = "passive";
|
|
344
|
+
return type;
|
|
345
|
+
};
|
|
346
|
+
const demoted = new Set<string>(); // `${instance}:${pad}`: a power output that is not the net's source
|
|
347
|
+
const sources = new Map<string, { c: BoardComponent; pad: string }[]>(); // net → its power outputs
|
|
348
|
+
for (const c of comps) {
|
|
349
|
+
const seen = new Set<string>();
|
|
350
|
+
for (const [pad, iface] of [...input.pads.get(c.instance)!].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))) {
|
|
351
|
+
const net = netsOf(c.instance)?.get(pad);
|
|
352
|
+
if (!net || PASSIVES.has(c.kind) || baseType(c, pad, iface) !== "power_out") continue;
|
|
353
|
+
// one part's several output pins on one rail (VOUT on two pads) are one source
|
|
354
|
+
if (seen.has(net)) demoted.add(`${c.instance}:${pad}`);
|
|
355
|
+
else (seen.add(net), sources.set(net, [...(sources.get(net) ?? []), { c, pad }]));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// a connector carries the power a part on the board makes (a battery or speaker lead): the part is the source
|
|
359
|
+
for (const list of sources.values()) if (list.some((x) => x.c.kind !== "connector")) for (const x of list) if (x.c.kind === "connector") demoted.add(`${x.c.instance}:${x.pad}`);
|
|
360
|
+
for (const c of comps) {
|
|
361
|
+
const pads = input.pads.get(c.instance)!;
|
|
362
|
+
const prefix = (input.refdes[c.instance].match(/^[A-Z#]+/)?.[0] ?? "U") || "U";
|
|
363
|
+
const s = symbolSpec(c, pads, (pad, iface) => (demoted.has(`${c.instance}:${pad}`) ? "passive" : baseType(c, pad, iface)), (pad) => netsOf(c.instance)?.get(pad), prefix);
|
|
364
|
+
const key = JSON.stringify([c.part, prefix, s.kind, s.passive, s.pins, s.left.map((p) => p.pad)]);
|
|
365
|
+
let spec = byKey.get(key);
|
|
366
|
+
if (!spec) {
|
|
367
|
+
const base = c.part.replace(/[:/\\\s"]+/g, "_");
|
|
368
|
+
let name = base;
|
|
369
|
+
for (let n = 2; names.has(name); n++) name = `${base}_${n}`;
|
|
370
|
+
names.add(name);
|
|
371
|
+
spec = { ...s, name };
|
|
372
|
+
byKey.set(key, spec);
|
|
373
|
+
}
|
|
374
|
+
specs.set(c.instance, spec);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- blocks: the board's own parts, then each sub-module in tree order
|
|
378
|
+
const groups = [{ path: "", name: board }, ...(netlist.groups ?? []).map((g) => ({ path: g.path, name: g.name }))];
|
|
379
|
+
const isIc = (c: BoardComponent) => specs.get(c.instance)!.kind === "box";
|
|
380
|
+
const netKind = new Map(netlist.nets.map((n) => [n.name, n.kind]));
|
|
381
|
+
const netKindOf = (name: string) => netKind.get(name) ?? (/gnd|ground/i.test(name) ? "ground" : "signal");
|
|
382
|
+
const blocks: { path: string; title: string; subtitle: string; box: Box }[] = [];
|
|
383
|
+
for (const g of groups) {
|
|
384
|
+
const members = comps.filter((c) => (c.group ?? "") === g.path);
|
|
385
|
+
if (!members.length) continue;
|
|
386
|
+
const ics = members.filter(isIc);
|
|
387
|
+
const passives = members.filter((c) => !isIc(c));
|
|
388
|
+
// each passive beside the IC it shares the most with: a signal net counts most, a supply some, ground not at all
|
|
389
|
+
const beside = new Map<string, BoardComponent[]>(ics.map((c) => [c.instance, []]));
|
|
390
|
+
const loose: BoardComponent[] = [];
|
|
391
|
+
for (const p of passives) {
|
|
392
|
+
const mine = [...(netsOf(p.instance)?.values() ?? [])];
|
|
393
|
+
let best: BoardComponent | undefined;
|
|
394
|
+
let score = 0;
|
|
395
|
+
for (const ic of ics) {
|
|
396
|
+
const theirs = new Set(netsOf(ic.instance)?.values() ?? []);
|
|
397
|
+
const s = mine.filter((n) => theirs.has(n)).reduce((a, n) => a + (netKindOf(n) === "signal" ? 3 : netKindOf(n) === "power" ? 1 : 0), 0);
|
|
398
|
+
if (s > score) (score = s), (best = ic);
|
|
399
|
+
}
|
|
400
|
+
if (best) beside.get(best.instance)!.push(p);
|
|
401
|
+
else loose.push(p);
|
|
402
|
+
}
|
|
403
|
+
// clusters: an IC and its passives in columns to its right; the loose passives in columns of their own
|
|
404
|
+
const clusters: Box[] = [];
|
|
405
|
+
const column = (parts: BoardComponent[], perColumn: number, x0: number, y0: number, items: Placed[]): { w: number; h: number } => {
|
|
406
|
+
let x = x0;
|
|
407
|
+
let h = 0;
|
|
408
|
+
for (let i = 0; i < parts.length; i += perColumn) {
|
|
409
|
+
const col = parts.slice(i, i + perColumn);
|
|
410
|
+
const ex = col.map((c) => extent(specs.get(c.instance)!, netsOf(c.instance)));
|
|
411
|
+
const l = up(Math.max(...ex.map((e) => e.l)));
|
|
412
|
+
const rr = up(Math.max(...ex.map((e) => e.r)));
|
|
413
|
+
col.forEach((c, k) => items.push({ inst: c.instance, spec: specs.get(c.instance)!, x: x + l, y: y0 + 2 * G + k * 3 * G }));
|
|
414
|
+
h = Math.max(h, col.length * 3 * G + G);
|
|
415
|
+
x += l + rr + G;
|
|
416
|
+
}
|
|
417
|
+
return { w: x - x0, h };
|
|
418
|
+
};
|
|
419
|
+
for (const ic of ics) {
|
|
420
|
+
const spec = specs.get(ic.instance)!;
|
|
421
|
+
const e = extent(spec, netsOf(ic.instance));
|
|
422
|
+
const items: Placed[] = [{ inst: ic.instance, spec, x: up(e.l), y: up(e.t) }];
|
|
423
|
+
const icW = up(e.l) + up(e.r);
|
|
424
|
+
const icH = up(e.t) + up(e.b);
|
|
425
|
+
const mine = beside.get(ic.instance)!;
|
|
426
|
+
const per = Math.max(4, Math.floor(icH / (3 * G)));
|
|
427
|
+
const col = mine.length ? column(mine, per, icW + G, 0, items) : { w: 0, h: 0 };
|
|
428
|
+
clusters.push({ w: icW + (mine.length ? G + col.w : 0), h: Math.max(icH, col.h), items });
|
|
429
|
+
}
|
|
430
|
+
if (loose.length) {
|
|
431
|
+
const items: Placed[] = [];
|
|
432
|
+
const per = Math.max(4, Math.ceil(Math.sqrt(loose.length * 3)));
|
|
433
|
+
const col = column(loose, per, 0, 0, items);
|
|
434
|
+
clusters.push({ w: col.w, h: col.h, items });
|
|
435
|
+
}
|
|
436
|
+
const area = clusters.reduce((a, c) => a + c.w * c.h, 0);
|
|
437
|
+
const width = Math.max(...clusters.map((c) => c.w), Math.sqrt(area) * 1.4);
|
|
438
|
+
const laid = shelf(clusters, width, 2 * G);
|
|
439
|
+
const items: Placed[] = [];
|
|
440
|
+
let w = 0;
|
|
441
|
+
let h = 0;
|
|
442
|
+
for (const { box, x, y } of laid) {
|
|
443
|
+
for (const it of box.items) items.push({ ...it, x: it.x + x, y: it.y + y });
|
|
444
|
+
w = Math.max(w, x + box.w);
|
|
445
|
+
h = Math.max(h, y + box.h);
|
|
446
|
+
}
|
|
447
|
+
blocks.push({ path: g.path, title: g.path || board, subtitle: g.path ? g.name : "the board's own parts", box: { w: up(w), h: up(h), items } });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---- the nets ERC should see as powered: a power input and no power output on them
|
|
451
|
+
const typeAt = new Map<string, KicadPinType[]>(); // net → pin types on it
|
|
452
|
+
for (const c of comps) {
|
|
453
|
+
const nets = netsOf(c.instance);
|
|
454
|
+
for (const p of specs.get(c.instance)!.pins) {
|
|
455
|
+
const n = nets?.get(p.pad);
|
|
456
|
+
if (n) typeAt.set(n, [...(typeAt.get(n) ?? []), p.type]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const flagged = [...typeAt.entries()].filter(([, ts]) => ts.includes("power_in") && !ts.includes("power_out")).map(([n]) => n).sort((a, b) => a.localeCompare(b));
|
|
460
|
+
if (flagged.length) {
|
|
461
|
+
const items: Placed[] = [];
|
|
462
|
+
const per = Math.max(4, Math.ceil(Math.sqrt(flagged.length * 2)));
|
|
463
|
+
const colW = up(Math.max(...flagged.map((n) => 2 * G + labelW(n)))) + 2 * G;
|
|
464
|
+
flagged.forEach((n, i) => items.push({ inst: `#flag:${n}`, spec: { name: "PWR_FLAG", prefix: "#FLG", kind: "two", pins: [], left: [], right: [], hw: 0, rows: 0 }, x: G + Math.floor(i / per) * colW, y: 4 * G + (i % per) * 4 * G }));
|
|
465
|
+
const cols = Math.ceil(flagged.length / per);
|
|
466
|
+
blocks.push({ path: "#flags", title: "Power flags", subtitle: "nets with a power input and no power output", box: { w: cols * colW, h: Math.min(per, flagged.length) * 4 * G + 2 * G, items } });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---- the sheet: blocks in rows, each framed with its title
|
|
470
|
+
const PAD = 2 * G; // inside a block's frame
|
|
471
|
+
const TITLE = 4 * G; // the title (sub-module path) and subtitle (its name) above a block's contents
|
|
472
|
+
const titleW = (t: string) => t.length * 2.54 * 0.95;
|
|
473
|
+
const framed = blocks.map((b) => ({ ...b, w: up(Math.max(b.box.w + 2 * PAD, titleW(b.title) + 2 * PAD, textW(b.subtitle) + 2 * PAD)), h: b.box.h + 2 * PAD + TITLE }));
|
|
474
|
+
const MARGIN = 10 * G; // the drawing frame
|
|
475
|
+
const TITLE_BLOCK = 12 * G; // KiCad's title block, bottom right
|
|
476
|
+
const GAP = 4 * G;
|
|
477
|
+
// the smallest sheet the blocks fit on, packed to its width; past A0, a user size of A0's shape
|
|
478
|
+
const pack = (width: number) => {
|
|
479
|
+
const laid = shelf(framed, width, GAP);
|
|
480
|
+
return { laid, w: Math.max(...laid.map((l) => l.x + l.box.w)), h: Math.max(...laid.map((l) => l.y + l.box.h)) };
|
|
481
|
+
};
|
|
482
|
+
let chosen: { laid: ReturnType<typeof shelf<(typeof framed)[number]>>; w: number; h: number; paper: List } | undefined;
|
|
483
|
+
for (const [name, pw, ph] of PAPERS) {
|
|
484
|
+
const p = pack(pw - 2 * MARGIN);
|
|
485
|
+
if (p.w + 2 * MARGIN <= pw && p.h + 2 * MARGIN + TITLE_BLOCK <= ph) {
|
|
486
|
+
chosen = { ...p, paper: list("paper", str(name)) };
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (!chosen) {
|
|
491
|
+
const area = framed.reduce((a, b) => a + (b.w + GAP) * (b.h + GAP), 0);
|
|
492
|
+
const p = pack(Math.max(...framed.map((b) => b.w), Math.sqrt(area * 1.414) * 1.1));
|
|
493
|
+
chosen = { ...p, paper: list("paper", str("User"), r(up(Math.max(p.w + 2 * MARGIN, (p.h + 2 * MARGIN + TITLE_BLOCK) * 1.414))), r(up(Math.max(p.h + 2 * MARGIN + TITLE_BLOCK, (p.w + 2 * MARGIN) / 1.414)))) };
|
|
494
|
+
}
|
|
495
|
+
const laid = chosen.laid;
|
|
496
|
+
const paperNode = chosen.paper;
|
|
497
|
+
|
|
498
|
+
// ---- the document
|
|
499
|
+
const libSymbols = [...new Map([...specs.values()].map((s) => [s.name, s])).values()].sort((a, b) => a.name.localeCompare(b.name)).map((s) => libSymbol(s, lib));
|
|
500
|
+
if (flagged.length) libSymbols.push(pwrFlag(lib));
|
|
501
|
+
const root = uuid("schematic");
|
|
502
|
+
const sch = list(
|
|
503
|
+
"kicad_sch",
|
|
504
|
+
list("version", "20250114"),
|
|
505
|
+
list("generator", str("protoboard")),
|
|
506
|
+
list("generator_version", str("0.1")),
|
|
507
|
+
list("uuid", root),
|
|
508
|
+
paperNode,
|
|
509
|
+
list("title_block", list("title", str(board)), list("comment", "1", str("From the UHD board (@protoboardai/ext-tscircuit)"))),
|
|
510
|
+
list("lib_symbols", ...libSymbols),
|
|
511
|
+
);
|
|
512
|
+
const drawn: List[] = [];
|
|
513
|
+
const labels: List[] = [];
|
|
514
|
+
const symbols: List[] = [];
|
|
515
|
+
const symbolOf: Record<string, string> = {};
|
|
516
|
+
const pinNames = new Map<string, Map<string, string>>();
|
|
517
|
+
let flagN = 0;
|
|
518
|
+
for (const { box: b, x: bx, y: by } of laid) {
|
|
519
|
+
const ox = MARGIN + bx;
|
|
520
|
+
const oy = MARGIN + by;
|
|
521
|
+
drawn.push(list("rectangle", list("start", r(ox), r(oy)), list("end", r(ox + b.w), r(oy + b.h)), stroke(0.254, "dash"), list("fill", list("type", "none")), list("uuid", uuid(`block:${b.path}`))));
|
|
522
|
+
drawn.push(list("text", str(b.title), list("exclude_from_sim", "no"), at(ox + PAD, oy + PAD + G), list("effects", list("font", list("size", "2.54", "2.54"), list("thickness", "0.508"), list("bold", "yes")), list("justify", "left", "bottom")), list("uuid", uuid(`title:${b.path}`))));
|
|
523
|
+
drawn.push(list("text", str(b.subtitle), list("exclude_from_sim", "no"), at(ox + PAD, oy + PAD + 2 * G), list("effects", list("font", list("size", r(FONT), r(FONT)), list("italic", "yes")), list("justify", "left", "bottom")), list("uuid", uuid(`subtitle:${b.path}`))));
|
|
524
|
+
const cx = ox + PAD;
|
|
525
|
+
const cy = oy + PAD + TITLE;
|
|
526
|
+
for (const it of b.box.items) {
|
|
527
|
+
const X = cx + it.x;
|
|
528
|
+
const Y = cy + it.y;
|
|
529
|
+
if (it.inst.startsWith("#flag:")) {
|
|
530
|
+
const net = it.inst.slice("#flag:".length);
|
|
531
|
+
const ref = `#FLG${String(++flagN).padStart(2, "0")}`;
|
|
532
|
+
const u = uuid(`flag:${net}`);
|
|
533
|
+
symbols.push(
|
|
534
|
+
list(
|
|
535
|
+
"symbol",
|
|
536
|
+
list("lib_id", str(`${lib}:PWR_FLAG`)),
|
|
537
|
+
at(X, Y),
|
|
538
|
+
list("unit", "1"),
|
|
539
|
+
list("exclude_from_sim", "no"),
|
|
540
|
+
list("in_bom", "yes"),
|
|
541
|
+
list("on_board", "yes"),
|
|
542
|
+
list("dnp", "no"),
|
|
543
|
+
list("uuid", u),
|
|
544
|
+
list("property", str("Reference"), str(ref), at(X, Y - 1.905), effects(FONT, undefined, true)),
|
|
545
|
+
list("property", str("Value"), str("PWR_FLAG"), at(X, Y - 3.81), effects()),
|
|
546
|
+
list("property", str("Footprint"), str(""), at(X, Y), effects(FONT, undefined, true)),
|
|
547
|
+
list("property", str("Datasheet"), str("~"), at(X, Y), effects(FONT, undefined, true)),
|
|
548
|
+
list("property", str("Description"), str(""), at(X, Y), effects(FONT, undefined, true)),
|
|
549
|
+
list("pin", str("1"), list("uuid", uuid(`flag-pin:${net}`))),
|
|
550
|
+
list("instances", list("project", str(board), list("path", str(`/${root}`), list("reference", str(ref)), list("unit", "1")))),
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
labels.push(...stubAndLabel(X, Y, 0, net, `flag:${net}`, uuid));
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const c = comps.find((x) => x.instance === it.inst)!;
|
|
557
|
+
const ref = input.refdes[c.instance];
|
|
558
|
+
const libId = `${lib}:${it.spec.name}`;
|
|
559
|
+
symbolOf[c.instance] = libId;
|
|
560
|
+
if (!pinNames.has(libId)) pinNames.set(libId, new Map(it.spec.pins.map((p) => [p.pad, p.name])));
|
|
561
|
+
const nets = netsOf(c.instance);
|
|
562
|
+
const places = pinPlaces(it.spec);
|
|
563
|
+
for (const p of it.spec.pins) {
|
|
564
|
+
const pl = places.get(p.pad)!;
|
|
565
|
+
const [px, py] = [X + pl.x, Y - pl.y];
|
|
566
|
+
const net = nets?.get(p.pad);
|
|
567
|
+
if (!net) {
|
|
568
|
+
labels.push(list("no_connect", list("at", r(px), r(py)), list("uuid", uuid(`nc:${ref}:${p.pad}`))));
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// the label points away from the body: left pins to the left, right pins to the right
|
|
572
|
+
labels.push(...stubAndLabel(px, py, pl.angle === 0 ? 180 : 0, net, `${ref}:${p.pad}`, uuid));
|
|
573
|
+
}
|
|
574
|
+
const two = it.spec.kind === "two";
|
|
575
|
+
const refAt = two ? at(X, Y - 1.905) : at(X - it.spec.hw, Y - G - 0.635);
|
|
576
|
+
const valAt = two ? at(X, Y + 1.905) : at(X - it.spec.hw, Y + it.spec.rows * G + 0.635);
|
|
577
|
+
const fields = input.fields[c.instance] ?? [["Value", c.name]];
|
|
578
|
+
const value = fields.find(([k]) => k === "Value")?.[1] ?? c.name;
|
|
579
|
+
symbols.push(
|
|
580
|
+
list(
|
|
581
|
+
"symbol",
|
|
582
|
+
list("lib_id", str(libId)),
|
|
583
|
+
at(X, Y),
|
|
584
|
+
list("unit", "1"),
|
|
585
|
+
list("exclude_from_sim", "no"),
|
|
586
|
+
list("in_bom", "yes"),
|
|
587
|
+
list("on_board", "yes"),
|
|
588
|
+
list("dnp", "no"),
|
|
589
|
+
list("uuid", input.symbolUuid[c.instance]),
|
|
590
|
+
list("property", str("Reference"), str(ref), refAt, effects(FONT, two ? ["bottom"] : ["left", "bottom"])),
|
|
591
|
+
list("property", str("Value"), str(value), valAt, effects(FONT, two ? ["top"] : ["left", "top"])),
|
|
592
|
+
list("property", str("Footprint"), str(input.footprint(c.instance)), at(X, Y), effects(FONT, undefined, true)),
|
|
593
|
+
list("property", str("Datasheet"), str(""), at(X, Y), effects(FONT, undefined, true)),
|
|
594
|
+
list("property", str("Description"), str(c.name), at(X, Y), effects(FONT, undefined, true)),
|
|
595
|
+
...fields.filter(([k]) => k !== "Value").map(([k, v]) => list("property", str(k), str(v), at(X, Y), effects(FONT, undefined, true))),
|
|
596
|
+
...it.spec.pins.map((p) => list("pin", str(p.pad), list("uuid", uuid(`pin:${c.instance}:${p.pad}`)))),
|
|
597
|
+
list("instances", list("project", str(board), list("path", str(`/${root}`), list("reference", str(ref)), list("unit", "1")))),
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
sch.items.push(...drawn, ...labels, ...symbols, list("sheet_instances", list("path", str("/"), list("page", str("1")))), list("embedded_fonts", "no"));
|
|
603
|
+
return { sch, libSymbols, symbolOf, pinNames };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** A short wire from a pin outward and a global label at its end, named by the net. */
|
|
607
|
+
function stubAndLabel(x: number, y: number, angle: 0 | 180, net: string, key: string, uuid: (k: string) => string): List[] {
|
|
608
|
+
const dx = angle === 0 ? STUB : -STUB;
|
|
609
|
+
const lx = x + dx;
|
|
610
|
+
return [
|
|
611
|
+
list("wire", list("pts", list("xy", r(x), r(y)), list("xy", r(lx), r(y))), stroke(0.1524), list("uuid", uuid(`stub:${key}`))),
|
|
612
|
+
list(
|
|
613
|
+
"global_label",
|
|
614
|
+
str(net),
|
|
615
|
+
list("shape", "passive"),
|
|
616
|
+
at(lx, y, angle),
|
|
617
|
+
list("fields_autoplaced", "yes"),
|
|
618
|
+
effects(FONT, [angle === 0 ? "left" : "right"]),
|
|
619
|
+
list("uuid", uuid(`label:${key}`)),
|
|
620
|
+
list("property", str("Intersheetrefs"), str("${INTERSHEET_REFS}"), at(lx, y, 0), effects(FONT, undefined, true)),
|
|
621
|
+
),
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
|