copperhead 0.8.1 → 0.10.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.
- package/NOTICE +1 -1
- package/README.md +13 -5
- package/dist/agent/filetools.js +24 -1
- package/dist/agent/filetools.js.map +1 -1
- package/dist/agent/ledger.js +24 -0
- package/dist/agent/ledger.js.map +1 -1
- package/dist/agent/loop.js +67 -62
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +4 -3
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/openai.js +28 -6
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/providers/tool-protocol.js +21 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -1
- package/dist/agent/recovery.js +95 -1
- package/dist/agent/recovery.js.map +1 -1
- package/dist/agent/response-cache.js +18 -2
- package/dist/agent/response-cache.js.map +1 -1
- package/dist/agent/tools.js +185 -1
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js +2 -0
- package/dist/agent/transcript.js.map +1 -1
- package/dist/cli.js +77 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.js +33 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/create.js +282 -26
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +211 -11
- package/dist/commands/doctor.js.map +1 -1
- package/dist/config.js +61 -4
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +24 -3
- package/dist/kicad/bootstrap.js.map +1 -1
- package/dist/kicad/cli.js +7 -26
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/dossier.js +207 -0
- package/dist/kicad/dossier.js.map +1 -0
- package/dist/kicad/draft/draft.js +132 -0
- package/dist/kicad/draft/draft.js.map +1 -0
- package/dist/kicad/draft/engine.js +2389 -0
- package/dist/kicad/draft/engine.js.map +1 -0
- package/dist/kicad/draft/ir.js +368 -0
- package/dist/kicad/draft/ir.js.map +1 -0
- package/dist/kicad/draft/symsource.js +490 -0
- package/dist/kicad/draft/symsource.js.map +1 -0
- package/dist/kicad/emit.js +181 -0
- package/dist/kicad/emit.js.map +1 -0
- package/dist/kicad/fab.js +13 -0
- package/dist/kicad/fab.js.map +1 -1
- package/dist/kicad/legibility.js +561 -0
- package/dist/kicad/legibility.js.map +1 -0
- package/dist/kicad/score.js +261 -0
- package/dist/kicad/score.js.map +1 -0
- package/dist/kicad/sexp.js +262 -10
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/kicad/symlib.js +346 -16
- package/dist/kicad/symlib.js.map +1 -1
- package/dist/memory/bom-table.js +108 -34
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/scaffold.js +6 -0
- package/dist/memory/scaffold.js.map +1 -1
- package/dist/openspec/cli.js +2 -1
- package/dist/openspec/cli.js.map +1 -1
- package/dist/util/preflight.js +17 -0
- package/dist/util/preflight.js.map +1 -1
- package/dist/util/redact.js +12 -2
- package/dist/util/redact.js.map +1 -1
- package/package.json +9 -7
- package/src/agent/filetools.ts +26 -1
- package/src/agent/ledger.ts +24 -0
- package/src/agent/loop.ts +88 -65
- package/src/agent/prompts.ts +4 -3
- package/src/agent/providers/openai.ts +38 -4
- package/src/agent/providers/tool-protocol.ts +22 -0
- package/src/agent/recovery.ts +94 -1
- package/src/agent/response-cache.ts +17 -1
- package/src/agent/tools.ts +189 -1
- package/src/agent/transcript.ts +6 -0
- package/src/cli.ts +73 -2
- package/src/commands/check.ts +51 -1
- package/src/commands/create.ts +278 -22
- package/src/commands/doctor.ts +219 -12
- package/src/config.ts +107 -2
- package/src/kicad/bootstrap.ts +24 -3
- package/src/kicad/cli.ts +6 -19
- package/src/kicad/dossier.ts +217 -0
- package/src/kicad/draft/draft.ts +171 -0
- package/src/kicad/draft/engine.ts +2466 -0
- package/src/kicad/draft/ir.ts +416 -0
- package/src/kicad/draft/symsource.ts +535 -0
- package/src/kicad/emit.ts +236 -0
- package/src/kicad/fab.ts +15 -0
- package/src/kicad/legibility.ts +646 -0
- package/src/kicad/score.ts +323 -0
- package/src/kicad/sexp.ts +339 -10
- package/src/kicad/symlib.ts +364 -18
- package/src/memory/bom-table.ts +119 -31
- package/src/memory/scaffold.ts +6 -0
- package/src/openspec/cli.ts +3 -2
- package/src/util/preflight.ts +18 -0
- package/src/util/redact.ts +12 -2
- package/dist/memory/synap.js +0 -152
- package/dist/memory/synap.js.map +0 -1
- package/src/memory/synap.ts +0 -217
package/src/kicad/sexp.ts
CHANGED
|
@@ -22,7 +22,11 @@ export function parseSexp(text: string): SexpNode[] {
|
|
|
22
22
|
let s = '';
|
|
23
23
|
while (j < text.length && text[j] !== '"') {
|
|
24
24
|
if (text[j] === '\\' && j + 1 < text.length) {
|
|
25
|
-
|
|
25
|
+
// KiCad writes multi-line text as `\n` (and tabs as `\t`) inside the
|
|
26
|
+
// quoted atom; mapping the escape to the literal letter would give
|
|
27
|
+
// the legibility width model a one-line `line1nline2` string.
|
|
28
|
+
const e = text[j + 1];
|
|
29
|
+
s += e === 'n' ? '\n' : e === 't' ? '\t' : e!;
|
|
26
30
|
j += 2;
|
|
27
31
|
} else {
|
|
28
32
|
s += text[j];
|
|
@@ -88,6 +92,8 @@ export interface SchematicSymbol {
|
|
|
88
92
|
sheet: string;
|
|
89
93
|
at: { x: number; y: number; rot: number };
|
|
90
94
|
uuid: string;
|
|
95
|
+
/** `(unit N)` of the placed instance; 1 for single-unit symbols. */
|
|
96
|
+
unit: number;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export interface PinDef {
|
|
@@ -95,6 +101,9 @@ export interface PinDef {
|
|
|
95
101
|
name: string;
|
|
96
102
|
x: number;
|
|
97
103
|
y: number;
|
|
104
|
+
/** Unit the pin belongs to (from the `_<unit>_<style>` child it sits in);
|
|
105
|
+
* 0 for a pin in the common unit, drawn on every placed unit. */
|
|
106
|
+
unit: number;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
interface ParsedSheet {
|
|
@@ -126,7 +135,10 @@ async function loadSheets(rootSch: string): Promise<ParsedSheet[]> {
|
|
|
126
135
|
return out;
|
|
127
136
|
}
|
|
128
137
|
|
|
129
|
-
/** Pin definitions per lib symbol name, in symbol coordinates.
|
|
138
|
+
/** Pin definitions per lib symbol name, in symbol coordinates. Each pin
|
|
139
|
+
* carries the unit of the `_<unit>_<style>` child it was defined in (0 for
|
|
140
|
+
* the common unit); de Morgan alternates (style ≥ 2) are skipped so a pin is
|
|
141
|
+
* recorded once. */
|
|
130
142
|
function libPinDefs(root: SexpNode[]): Map<string, PinDef[]> {
|
|
131
143
|
const map = new Map<string, PinDef[]>();
|
|
132
144
|
const libs = child(root, 'lib_symbols');
|
|
@@ -135,8 +147,15 @@ function libPinDefs(root: SexpNode[]): Map<string, PinDef[]> {
|
|
|
135
147
|
const name = atomAt(sym, 1);
|
|
136
148
|
if (!name) continue;
|
|
137
149
|
const pins: PinDef[] = [];
|
|
138
|
-
const walk = (n: SexpNode): void => {
|
|
150
|
+
const walk = (n: SexpNode, unit: number): void => {
|
|
139
151
|
if (!isList(n)) return;
|
|
152
|
+
if (tag(n) === 'symbol') {
|
|
153
|
+
const m = /_(\d+)_(\d+)$/.exec(atomAt(n, 1) ?? '');
|
|
154
|
+
if (m) {
|
|
155
|
+
if (Number(m[2]) >= 2) return; // de Morgan alternate body style
|
|
156
|
+
unit = Number(m[1]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
140
159
|
if (tag(n) === 'pin') {
|
|
141
160
|
const at = child(n, 'at');
|
|
142
161
|
const num = atomAt(child(n, 'number'), 1);
|
|
@@ -147,17 +166,26 @@ function libPinDefs(root: SexpNode[]): Map<string, PinDef[]> {
|
|
|
147
166
|
name: pinName ?? '~',
|
|
148
167
|
x: parseFloat(atomAt(at, 1) ?? '0'),
|
|
149
168
|
y: parseFloat(atomAt(at, 2) ?? '0'),
|
|
169
|
+
unit,
|
|
150
170
|
});
|
|
151
171
|
}
|
|
152
172
|
}
|
|
153
|
-
for (const c of n) walk(c);
|
|
173
|
+
for (const c of n) walk(c, unit);
|
|
154
174
|
};
|
|
155
|
-
walk(sym);
|
|
175
|
+
walk(sym, 0);
|
|
156
176
|
map.set(name, pins);
|
|
157
177
|
}
|
|
158
178
|
return map;
|
|
159
179
|
}
|
|
160
180
|
|
|
181
|
+
/** The pins a PLACED instance actually shows: its own unit's plus the common
|
|
182
|
+
* unit's. A single-unit symbol keeps every pin (they are all unit ≤ 1). */
|
|
183
|
+
export function pinsOfUnit(defs: PinDef[], unit: number): PinDef[] {
|
|
184
|
+
const multi = defs.some((p) => p.unit >= 2);
|
|
185
|
+
if (!multi) return defs;
|
|
186
|
+
return defs.filter((p) => p.unit === 0 || p.unit === unit);
|
|
187
|
+
}
|
|
188
|
+
|
|
161
189
|
/** Symbol-space → schematic-space transform (schematic Y grows downward). */
|
|
162
190
|
export function pinAbsolute(
|
|
163
191
|
symAt: { x: number; y: number; rot: number },
|
|
@@ -218,21 +246,40 @@ function symbolsOf(sheet: ParsedSheet): { node: SexpNode[]; sym: SchematicSymbol
|
|
|
218
246
|
rot: parseFloat(atomAt(at, 3) ?? '0'),
|
|
219
247
|
},
|
|
220
248
|
uuid: atomAt(child(s, 'uuid'), 1) ?? '',
|
|
249
|
+
unit: parseInt(atomAt(child(s, 'unit'), 1) ?? '1', 10) || 1,
|
|
221
250
|
},
|
|
222
251
|
});
|
|
223
252
|
}
|
|
224
253
|
return out;
|
|
225
254
|
}
|
|
226
255
|
|
|
227
|
-
|
|
256
|
+
function collectPowerSymbols(sheets: ParsedSheet[]): Set<string> {
|
|
257
|
+
const set = new Set<string>();
|
|
258
|
+
for (const sheet of sheets) {
|
|
259
|
+
const libs = child(sheet.root, 'lib_symbols');
|
|
260
|
+
if (!libs) continue;
|
|
261
|
+
for (const sym of children(libs, 'symbol')) {
|
|
262
|
+
const name = atomAt(sym, 1);
|
|
263
|
+
const p = child(sym, 'power');
|
|
264
|
+
if (name && p !== undefined && atomAt(p, 1) !== 'no') {
|
|
265
|
+
set.add(name);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return set;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const isPowerSymbol = (libId: string, powerSyms: Set<string>): boolean =>
|
|
273
|
+
libId.startsWith('power:') || powerSyms.has(libId);
|
|
228
274
|
|
|
229
275
|
/** One row per real component (power symbols excluded), across all sheets. */
|
|
230
276
|
export async function listSymbols(rootSch: string): Promise<SchematicSymbol[]> {
|
|
231
277
|
const sheets = await loadSheets(rootSch);
|
|
278
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
232
279
|
const out: SchematicSymbol[] = [];
|
|
233
280
|
for (const sheet of sheets) {
|
|
234
281
|
for (const { sym } of symbolsOf(sheet)) {
|
|
235
|
-
if (!isPowerSymbol(sym.libId)) out.push(sym);
|
|
282
|
+
if (!isPowerSymbol(sym.libId, powerSyms)) out.push(sym);
|
|
236
283
|
}
|
|
237
284
|
}
|
|
238
285
|
return out.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
|
@@ -241,6 +288,7 @@ export async function listSymbols(rootSch: string): Promise<SchematicSymbol[]> {
|
|
|
241
288
|
/** All net names visible via labels and power symbols, across all sheets. */
|
|
242
289
|
export async function listNets(rootSch: string): Promise<string[]> {
|
|
243
290
|
const sheets = await loadSheets(rootSch);
|
|
291
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
244
292
|
const names = new Set<string>();
|
|
245
293
|
for (const sheet of sheets) {
|
|
246
294
|
for (const kind of ['label', 'global_label', 'hierarchical_label']) {
|
|
@@ -250,7 +298,7 @@ export async function listNets(rootSch: string): Promise<string[]> {
|
|
|
250
298
|
}
|
|
251
299
|
}
|
|
252
300
|
for (const { sym } of symbolsOf(sheet)) {
|
|
253
|
-
if (isPowerSymbol(sym.libId)) names.add(sym.value);
|
|
301
|
+
if (isPowerSymbol(sym.libId, powerSyms)) names.add(sym.value);
|
|
254
302
|
}
|
|
255
303
|
}
|
|
256
304
|
return [...names].sort();
|
|
@@ -263,6 +311,283 @@ export interface PinNet {
|
|
|
263
311
|
net: string | null;
|
|
264
312
|
}
|
|
265
313
|
|
|
314
|
+
// ---------- Read-only geometry accessors for the legibility checker ----------
|
|
315
|
+
|
|
316
|
+
export interface Bounds {
|
|
317
|
+
minX: number;
|
|
318
|
+
minY: number;
|
|
319
|
+
maxX: number;
|
|
320
|
+
maxY: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface WireSeg {
|
|
324
|
+
x1: number;
|
|
325
|
+
y1: number;
|
|
326
|
+
x2: number;
|
|
327
|
+
y2: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface TextItem {
|
|
331
|
+
text: string;
|
|
332
|
+
x: number;
|
|
333
|
+
y: number;
|
|
334
|
+
rot: number;
|
|
335
|
+
/** Font height in mm (KiCad `(size h w)`); 1.27 when unstated. */
|
|
336
|
+
height: number;
|
|
337
|
+
hidden: boolean;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface LabelItem {
|
|
341
|
+
name: string;
|
|
342
|
+
kind: 'label' | 'global_label' | 'hierarchical_label';
|
|
343
|
+
x: number;
|
|
344
|
+
y: number;
|
|
345
|
+
rot: number;
|
|
346
|
+
height: number;
|
|
347
|
+
/** Horizontal component of `(justify …)`, or null when unspecified. KiCad
|
|
348
|
+
* normalizes angles 180/270 to 0/90 at draw time and the stored justify
|
|
349
|
+
* describes that DRAWN frame, so a leftward label appears as angle 180 (no
|
|
350
|
+
* justify), angle 180 + justify right, or angle 0 + justify right — all the
|
|
351
|
+
* same box. */
|
|
352
|
+
justify: 'left' | 'right' | null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface RectItem {
|
|
356
|
+
x1: number;
|
|
357
|
+
y1: number;
|
|
358
|
+
x2: number;
|
|
359
|
+
y2: number;
|
|
360
|
+
strokeType: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface PlacedSymbolGeom {
|
|
364
|
+
ref: string;
|
|
365
|
+
libId: string;
|
|
366
|
+
value: string;
|
|
367
|
+
at: { x: number; y: number; rot: number };
|
|
368
|
+
mirror: 'x' | 'y' | null;
|
|
369
|
+
isPower: boolean;
|
|
370
|
+
/** `(unit N)` of the placed instance; 1 for single-unit symbols. */
|
|
371
|
+
unit: number;
|
|
372
|
+
/** Reference and Value property text (absolute schematic coordinates). */
|
|
373
|
+
props: TextItem[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface SheetGeometry {
|
|
377
|
+
filePath: string;
|
|
378
|
+
sheetName: string;
|
|
379
|
+
/** `(paper …)`: standard name, or explicit size when the file states one. */
|
|
380
|
+
paper: { name: string | null; width: number | null; height: number | null; portrait: boolean };
|
|
381
|
+
titleBlock: { title: string; date: string; rev: string } | null;
|
|
382
|
+
symbols: PlacedSymbolGeom[];
|
|
383
|
+
wires: WireSeg[];
|
|
384
|
+
labels: LabelItem[];
|
|
385
|
+
/** Free `(text …)` items (group captions live here). */
|
|
386
|
+
texts: TextItem[];
|
|
387
|
+
/** Sheet-level `(rectangle …)` graphics (group boxes live here). */
|
|
388
|
+
rectangles: RectItem[];
|
|
389
|
+
/** Union bounds of each lib symbol's body graphics, symbol space; null when the entry draws nothing. */
|
|
390
|
+
libBounds: Map<string, Bounds | null>;
|
|
391
|
+
libPins: Map<string, PinDef[]>;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const num = (n: SexpNode[] | undefined, idx: number, fallback = 0): number => {
|
|
395
|
+
const v = atomAt(n, idx);
|
|
396
|
+
return v === undefined ? fallback : parseFloat(v);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
function effectsOf(node: SexpNode[]): { height: number; hidden: boolean } {
|
|
400
|
+
const effects = child(node, 'effects');
|
|
401
|
+
const font = effects ? child(effects, 'font') : undefined;
|
|
402
|
+
const size = font ? child(font, 'size') : undefined;
|
|
403
|
+
// hidden: legacy bare `hide` atom, or v9 `(hide yes)`
|
|
404
|
+
const hideNode = effects ? child(effects, 'hide') : undefined;
|
|
405
|
+
const hidden =
|
|
406
|
+
(effects?.some((c) => c === 'hide') ?? false) || (hideNode !== undefined && atomAt(hideNode, 1) !== 'no');
|
|
407
|
+
return { height: size ? num(size, 1, 1.27) : 1.27, hidden };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function libBodyBounds(root: SexpNode[]): Map<string, Bounds | null> {
|
|
411
|
+
const map = new Map<string, Bounds | null>();
|
|
412
|
+
const libs = child(root, 'lib_symbols');
|
|
413
|
+
if (!libs) return map;
|
|
414
|
+
for (const sym of children(libs, 'symbol')) {
|
|
415
|
+
const name = atomAt(sym, 1);
|
|
416
|
+
if (!name) continue;
|
|
417
|
+
/** Union across the whole symbol under `name`, per-unit union (common
|
|
418
|
+
* graphics included) under `name#<unit>` — a placed unit of a multi-unit
|
|
419
|
+
* symbol draws only its own unit's graphics, so measuring it with the
|
|
420
|
+
* package union would size every instance like the whole gate pack. */
|
|
421
|
+
const perUnit = new Map<number, Bounds | null>();
|
|
422
|
+
let b: Bounds | null = null;
|
|
423
|
+
const grow = (prev: Bounds | null, x: number, y: number): Bounds => {
|
|
424
|
+
if (!prev) return { minX: x, minY: y, maxX: x, maxY: y };
|
|
425
|
+
prev.minX = Math.min(prev.minX, x);
|
|
426
|
+
prev.minY = Math.min(prev.minY, y);
|
|
427
|
+
prev.maxX = Math.max(prev.maxX, x);
|
|
428
|
+
prev.maxY = Math.max(prev.maxY, y);
|
|
429
|
+
return prev;
|
|
430
|
+
};
|
|
431
|
+
const walk = (n: SexpNode, unit: number | null): void => {
|
|
432
|
+
if (!isList(n)) return;
|
|
433
|
+
const t = tag(n);
|
|
434
|
+
if (t === 'symbol') {
|
|
435
|
+
const m = /_(\d+)_(\d+)$/.exec(atomAt(n, 1) ?? '');
|
|
436
|
+
if (m) {
|
|
437
|
+
if (Number(m[2]) >= 2) return; // de Morgan alternate body style
|
|
438
|
+
unit = Number(m[1]);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const extend = (x: number, y: number): void => {
|
|
442
|
+
b = grow(b, x, y);
|
|
443
|
+
if (unit !== null) perUnit.set(unit, grow(perUnit.get(unit) ?? null, x, y));
|
|
444
|
+
};
|
|
445
|
+
if (t === 'rectangle') {
|
|
446
|
+
const s = child(n, 'start');
|
|
447
|
+
const e = child(n, 'end');
|
|
448
|
+
extend(num(s, 1), num(s, 2));
|
|
449
|
+
extend(num(e, 1), num(e, 2));
|
|
450
|
+
} else if (t === 'circle') {
|
|
451
|
+
const c = child(n, 'center');
|
|
452
|
+
const r = num(child(n, 'radius'), 1);
|
|
453
|
+
extend(num(c, 1) - r, num(c, 2) - r);
|
|
454
|
+
extend(num(c, 1) + r, num(c, 2) + r);
|
|
455
|
+
} else if (t === 'arc') {
|
|
456
|
+
for (const part of ['start', 'mid', 'end']) {
|
|
457
|
+
const p = child(n, part);
|
|
458
|
+
if (p) extend(num(p, 1), num(p, 2));
|
|
459
|
+
}
|
|
460
|
+
} else if (t === 'polyline') {
|
|
461
|
+
for (const xy of children(child(n, 'pts') ?? [], 'xy')) extend(num(xy, 1), num(xy, 2));
|
|
462
|
+
}
|
|
463
|
+
// pin/text graphics are deliberately excluded from the body box (design C2)
|
|
464
|
+
if (t !== 'pin' && t !== 'text') for (const c of n) walk(c, unit);
|
|
465
|
+
};
|
|
466
|
+
walk(sym, null);
|
|
467
|
+
map.set(name, b);
|
|
468
|
+
const common = perUnit.get(0) ?? null;
|
|
469
|
+
for (const [unit, ub] of perUnit) {
|
|
470
|
+
if (unit === 0) continue;
|
|
471
|
+
const merged =
|
|
472
|
+
ub && common
|
|
473
|
+
? {
|
|
474
|
+
minX: Math.min(ub.minX, common.minX),
|
|
475
|
+
minY: Math.min(ub.minY, common.minY),
|
|
476
|
+
maxX: Math.max(ub.maxX, common.maxX),
|
|
477
|
+
maxY: Math.max(ub.maxY, common.maxY),
|
|
478
|
+
}
|
|
479
|
+
: (ub ?? (common ? { ...common } : null));
|
|
480
|
+
map.set(`${name}#${unit}`, merged);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return map;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function textItemsOf(sym: SexpNode[]): TextItem[] {
|
|
487
|
+
const out: TextItem[] = [];
|
|
488
|
+
for (const p of children(sym, 'property')) {
|
|
489
|
+
const key = atomAt(p, 1);
|
|
490
|
+
if (key !== 'Reference' && key !== 'Value') continue;
|
|
491
|
+
const at = child(p, 'at');
|
|
492
|
+
const fx = effectsOf(p);
|
|
493
|
+
out.push({
|
|
494
|
+
text: atomAt(p, 2) ?? '',
|
|
495
|
+
x: num(at, 1),
|
|
496
|
+
y: num(at, 2),
|
|
497
|
+
rot: num(at, 3),
|
|
498
|
+
height: fx.height,
|
|
499
|
+
hidden: fx.hidden,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Everything the legibility checker needs to see per sheet, extracted read-only.
|
|
507
|
+
* Never serializes and never mutates the file.
|
|
508
|
+
*/
|
|
509
|
+
export async function readSheetGeometry(rootSch: string): Promise<SheetGeometry[]> {
|
|
510
|
+
const sheets = await loadSheets(rootSch);
|
|
511
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
512
|
+
return sheets.map((sheet) => {
|
|
513
|
+
const paperNode = child(sheet.root, 'paper');
|
|
514
|
+
const paperName = atomAt(paperNode, 1) ?? null;
|
|
515
|
+
const portrait = paperNode?.includes('portrait') ?? false;
|
|
516
|
+
const explicitW = num(paperNode, 2, NaN);
|
|
517
|
+
const explicitH = num(paperNode, 3, NaN);
|
|
518
|
+
const tb = child(sheet.root, 'title_block');
|
|
519
|
+
const tbField = (name: string): string => (tb ? (atomAt(child(tb, name), 1) ?? '') : '');
|
|
520
|
+
const wires: WireSeg[] = [];
|
|
521
|
+
for (const w of children(sheet.root, 'wire')) {
|
|
522
|
+
const pts = children(child(w, 'pts') ?? [], 'xy');
|
|
523
|
+
for (let i = 1; i < pts.length; i++) {
|
|
524
|
+
wires.push({ x1: num(pts[i - 1], 1), y1: num(pts[i - 1], 2), x2: num(pts[i], 1), y2: num(pts[i], 2) });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const labels: LabelItem[] = [];
|
|
528
|
+
for (const kind of ['label', 'global_label', 'hierarchical_label'] as const) {
|
|
529
|
+
for (const l of children(sheet.root, kind)) {
|
|
530
|
+
const at = child(l, 'at');
|
|
531
|
+
const effects = child(l, 'effects');
|
|
532
|
+
const justify = effects ? child(effects, 'justify') : undefined;
|
|
533
|
+
labels.push({
|
|
534
|
+
name: atomAt(l, 1) ?? '',
|
|
535
|
+
kind,
|
|
536
|
+
x: num(at, 1),
|
|
537
|
+
y: num(at, 2),
|
|
538
|
+
rot: num(at, 3),
|
|
539
|
+
height: effectsOf(l).height,
|
|
540
|
+
justify: justify?.some((c) => c === 'right') ? 'right' : justify?.some((c) => c === 'left') ? 'left' : null,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const texts: TextItem[] = children(sheet.root, 'text').map((t) => {
|
|
545
|
+
const at = child(t, 'at');
|
|
546
|
+
const fx = effectsOf(t);
|
|
547
|
+
return { text: atomAt(t, 1) ?? '', x: num(at, 1), y: num(at, 2), rot: num(at, 3), height: fx.height, hidden: fx.hidden };
|
|
548
|
+
});
|
|
549
|
+
const rectangles: RectItem[] = children(sheet.root, 'rectangle').map((r) => {
|
|
550
|
+
const s = child(r, 'start');
|
|
551
|
+
const e = child(r, 'end');
|
|
552
|
+
const stroke = child(r, 'stroke');
|
|
553
|
+
return {
|
|
554
|
+
x1: num(s, 1),
|
|
555
|
+
y1: num(s, 2),
|
|
556
|
+
x2: num(e, 1),
|
|
557
|
+
y2: num(e, 2),
|
|
558
|
+
strokeType: atomAt(child(stroke ?? [], 'type'), 1) ?? 'default',
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
filePath: sheet.filePath,
|
|
563
|
+
sheetName: sheet.sheetName,
|
|
564
|
+
paper: {
|
|
565
|
+
name: paperName,
|
|
566
|
+
width: Number.isNaN(explicitW) ? null : explicitW,
|
|
567
|
+
height: Number.isNaN(explicitH) ? null : explicitH,
|
|
568
|
+
portrait,
|
|
569
|
+
},
|
|
570
|
+
titleBlock: tb ? { title: tbField('title'), date: tbField('date'), rev: tbField('rev') } : null,
|
|
571
|
+
symbols: symbolsOf(sheet).map(({ sym, mirror, node }) => ({
|
|
572
|
+
ref: sym.ref,
|
|
573
|
+
libId: sym.libId,
|
|
574
|
+
value: sym.value,
|
|
575
|
+
at: sym.at,
|
|
576
|
+
mirror,
|
|
577
|
+
isPower: isPowerSymbol(sym.libId, powerSyms),
|
|
578
|
+
unit: sym.unit,
|
|
579
|
+
props: textItemsOf(node),
|
|
580
|
+
})),
|
|
581
|
+
wires,
|
|
582
|
+
labels,
|
|
583
|
+
texts,
|
|
584
|
+
rectangles,
|
|
585
|
+
libBounds: libBodyBounds(sheet.root),
|
|
586
|
+
libPins: libPinDefs(sheet.root),
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
266
591
|
/**
|
|
267
592
|
* Geometric connectivity per sheet: pins, labels, and wire endpoints that share
|
|
268
593
|
* coordinates (or are joined by wires) form a group; a group's net name comes
|
|
@@ -271,6 +596,7 @@ export interface PinNet {
|
|
|
271
596
|
*/
|
|
272
597
|
export async function pinNets(rootSch: string): Promise<PinNet[]> {
|
|
273
598
|
const sheets = await loadSheets(rootSch);
|
|
599
|
+
const powerSyms = collectPowerSymbols(sheets);
|
|
274
600
|
const out: PinNet[] = [];
|
|
275
601
|
for (const sheet of sheets) {
|
|
276
602
|
const pinDefs = libPinDefs(sheet.root);
|
|
@@ -299,12 +625,15 @@ export async function pinNets(rootSch: string): Promise<PinNet[]> {
|
|
|
299
625
|
|
|
300
626
|
const symPins: { sym: SchematicSymbol; pin: PinDef; k: string }[] = [];
|
|
301
627
|
for (const { sym, mirror } of symbolsOf(sheet)) {
|
|
302
|
-
|
|
628
|
+
// a placed unit of a multi-unit symbol shows only its own unit's pins;
|
|
629
|
+
// resolving the whole package at each instance would invent phantom
|
|
630
|
+
// connection points at every other unit's coordinates
|
|
631
|
+
const defs = pinsOfUnit(pinDefs.get(sym.libId) ?? [], sym.unit);
|
|
303
632
|
for (const pin of defs) {
|
|
304
633
|
const abs = pinAbsolute(sym.at, mirror, pin);
|
|
305
634
|
const k = key(abs.x, abs.y);
|
|
306
635
|
uf.find(k);
|
|
307
|
-
if (isPowerSymbol(sym.libId)) {
|
|
636
|
+
if (isPowerSymbol(sym.libId, powerSyms)) {
|
|
308
637
|
netNameAt.set(k, sym.value);
|
|
309
638
|
} else {
|
|
310
639
|
symPins.push({ sym, pin, k });
|