copperhead 0.10.0 → 0.11.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/README.md +41 -2
- package/dist/agent/context.js +2 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/dock-renderer.js +2 -2
- package/dist/agent/dock-renderer.js.map +1 -1
- package/dist/agent/envelope.js +105 -0
- package/dist/agent/envelope.js.map +1 -0
- package/dist/agent/loop.js +34 -14
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/providers/claude-code.js +17 -1
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/codex.js +84 -39
- package/dist/agent/providers/codex.js.map +1 -1
- package/dist/agent/recovery.js +91 -14
- package/dist/agent/recovery.js.map +1 -1
- package/dist/agent/registry.js +49 -0
- package/dist/agent/registry.js.map +1 -0
- package/dist/agent/render.js +2 -2
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/theme.js +10 -5
- package/dist/agent/theme.js.map +1 -1
- package/dist/agent/tools.js +99 -769
- package/dist/agent/tools.js.map +1 -1
- package/dist/capabilities/define.js +35 -0
- package/dist/capabilities/define.js.map +1 -0
- package/dist/capabilities/handlers.js +744 -0
- package/dist/capabilities/handlers.js.map +1 -0
- package/dist/capabilities/helpers.js +39 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/index.js +50 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/skills/generate-report.js +23 -0
- package/dist/capabilities/skills/generate-report.js.map +1 -0
- package/dist/cli.js +84 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +5 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +33 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/skill.js +109 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/sync.js +3 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/config.js +18 -6
- package/dist/config.js.map +1 -1
- package/dist/kicad/cli.js +106 -18
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/draft/draft.js +3 -0
- package/dist/kicad/draft/draft.js.map +1 -1
- package/dist/kicad/draft/engine.js +3139 -218
- package/dist/kicad/draft/engine.js.map +1 -1
- package/dist/kicad/draft/symsource.js +24 -10
- package/dist/kicad/draft/symsource.js.map +1 -1
- package/dist/kicad/emit.js +45 -6
- package/dist/kicad/emit.js.map +1 -1
- package/dist/kicad/legibility.js +51 -4
- package/dist/kicad/legibility.js.map +1 -1
- package/dist/kicad/score.js +173 -3
- package/dist/kicad/score.js.map +1 -1
- package/dist/kicad/sexp.js +32 -6
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/scaffold.js +8 -1
- package/dist/memory/scaffold.js.map +1 -1
- package/package.json +5 -2
- package/src/agent/context.ts +35 -0
- package/src/agent/dock-renderer.ts +3 -2
- package/src/agent/envelope.ts +124 -0
- package/src/agent/loop.ts +45 -17
- package/src/agent/providers/claude-code.ts +22 -1
- package/src/agent/providers/codex.ts +91 -42
- package/src/agent/recovery.ts +89 -12
- package/src/agent/registry.ts +58 -0
- package/src/agent/render.ts +4 -3
- package/src/agent/theme.ts +15 -5
- package/src/agent/tools.ts +124 -816
- package/src/agent/types.ts +10 -5
- package/src/capabilities/define.ts +88 -0
- package/src/capabilities/handlers.ts +769 -0
- package/src/capabilities/helpers.ts +37 -0
- package/src/capabilities/index.ts +53 -0
- package/src/capabilities/skills/generate-report.ts +25 -0
- package/src/cli.ts +84 -1
- package/src/commands/create.ts +5 -2
- package/src/commands/doctor.ts +34 -3
- package/src/commands/skill.ts +127 -0
- package/src/commands/sync.ts +5 -3
- package/src/config.ts +32 -8
- package/src/kicad/cli.ts +129 -18
- package/src/kicad/draft/draft.ts +2 -0
- package/src/kicad/draft/engine.ts +3034 -226
- package/src/kicad/draft/symsource.ts +24 -10
- package/src/kicad/emit.ts +71 -7
- package/src/kicad/legibility.ts +55 -6
- package/src/kicad/score.ts +187 -8
- package/src/kicad/sexp.ts +37 -6
- package/src/mcp/server.ts +560 -0
- package/src/memory/scaffold.ts +8 -1
|
@@ -308,14 +308,26 @@ export class SymbolSource {
|
|
|
308
308
|
return [...this.libs].sort();
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* Write an engine-generated symbol block into the vendored cache (power lib).
|
|
313
|
+
*
|
|
314
|
+
* A generated block is the engine's, not a vendor's: when the engine's
|
|
315
|
+
* drawing of a rail or ground changes, the cached copy must follow, or every
|
|
316
|
+
* embedded power symbol on the sheet raises a lib_symbol_mismatch warning
|
|
317
|
+
* against the stale library (the triangle-ground redraw hit 111 of them on
|
|
318
|
+
* esp32-amp). A block already cached byte-for-byte is left alone.
|
|
319
|
+
*/
|
|
312
320
|
async vendorGenerated(libId: string, block: string): Promise<void> {
|
|
313
321
|
const lib = libId.slice(0, libId.indexOf(':'));
|
|
314
322
|
const name = libId.slice(libId.indexOf(':') + 1);
|
|
315
323
|
const file = path.join(this.cacheDir(), vendorFileName(lib));
|
|
316
324
|
await mkdir(this.cacheDir(), { recursive: true });
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
let text = existsSync(file) ? await readFile(file, 'utf8') : emptyVendorLib();
|
|
326
|
+
const cached = extractSymbolBlock(text, name);
|
|
327
|
+
if (cached !== block) {
|
|
328
|
+
if (cached) text = text.replace(cached + '\n', '').replace(cached, '');
|
|
329
|
+
await writeFile(file, appendToVendorLib(text, block), 'utf8');
|
|
330
|
+
}
|
|
319
331
|
this.libs.add(lib);
|
|
320
332
|
}
|
|
321
333
|
|
|
@@ -473,15 +485,16 @@ export function powerSymbolSource(rawNet: string, kind: 'rail' | 'ground'): { li
|
|
|
473
485
|
const name = powerNetToken(rawNet);
|
|
474
486
|
const libId = `copperhead_power:${name}`;
|
|
475
487
|
// Symbol space is Y-up and the schematic transform flips it: negative-Y
|
|
476
|
-
// graphics here render BELOW the connection point on the sheet.
|
|
477
|
-
//
|
|
478
|
-
// above (angle 90)
|
|
488
|
+
// graphics here render BELOW the connection point on the sheet. A ground
|
|
489
|
+
// hangs below its pin (angle 270 = line toward -Y body) as a stem and an
|
|
490
|
+
// open triangle, the reference-ground glyph; a rail rises above (angle 90)
|
|
491
|
+
// as a stem and a filled arrowhead, the way KiCad's own +3V3 draws. Both
|
|
492
|
+
// reach 2.54 from the pin, inside the 2-unit box the engine reserves.
|
|
479
493
|
const body =
|
|
480
494
|
kind === 'ground'
|
|
481
495
|
? `\t(symbol "${name}_0_1"
|
|
482
|
-
\t\t(polyline (pts (xy
|
|
483
|
-
\t\t(polyline (pts (xy -
|
|
484
|
-
\t\t(polyline (pts (xy -0.254 -2.286) (xy 0.254 -2.286)) (stroke (width 0.254) (type default)) (fill (type none)))
|
|
496
|
+
\t\t(polyline (pts (xy 0 0) (xy 0 -1.27)) (stroke (width 0.254) (type default)) (fill (type none)))
|
|
497
|
+
\t\t(polyline (pts (xy -1.27 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27)) (stroke (width 0.254) (type default)) (fill (type none)))
|
|
485
498
|
\t)
|
|
486
499
|
\t(symbol "${name}_1_1"
|
|
487
500
|
\t\t(pin power_in line (at 0 0 270) (length 0) hide
|
|
@@ -490,7 +503,8 @@ export function powerSymbolSource(rawNet: string, kind: 'rail' | 'ground'): { li
|
|
|
490
503
|
\t\t)
|
|
491
504
|
\t)`
|
|
492
505
|
: `\t(symbol "${name}_0_1"
|
|
493
|
-
\t\t(polyline (pts (xy
|
|
506
|
+
\t\t(polyline (pts (xy 0 0) (xy 0 1.27)) (stroke (width 0.254) (type default)) (fill (type none)))
|
|
507
|
+
\t\t(polyline (pts (xy -0.762 1.27) (xy 0 2.54) (xy 0.762 1.27) (xy -0.762 1.27)) (stroke (width 0.254) (type default)) (fill (type outline)))
|
|
494
508
|
\t)
|
|
495
509
|
\t(symbol "${name}_1_1"
|
|
496
510
|
\t\t(pin power_in line (at 0 0 90) (length 0) hide
|
package/src/kicad/emit.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface EmitSymbol {
|
|
|
41
41
|
value: string;
|
|
42
42
|
footprint: string;
|
|
43
43
|
at: { x: number; y: number; rot: number };
|
|
44
|
+
/** KiCad `(mirror x|y)`: y flips the symbol left-for-right (a transistor whose base must face right). */
|
|
45
|
+
mirror?: 'x' | 'y';
|
|
44
46
|
/** Reference/Value text anchors (absolute). Hidden properties reuse the origin. */
|
|
45
47
|
refAt: { x: number; y: number };
|
|
46
48
|
valueAt: { x: number; y: number };
|
|
@@ -55,6 +57,38 @@ export interface EmitSymbol {
|
|
|
55
57
|
unit?: number;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Flag outline of a global label: KiCad's shape vocabulary, chosen from the
|
|
61
|
+
* electrical type of the pin the label serves (`shape` is cosmetic in ERC). */
|
|
62
|
+
export type LabelShape = 'input' | 'output' | 'bidirectional' | 'passive';
|
|
63
|
+
|
|
64
|
+
export interface EmitLabel {
|
|
65
|
+
name: string;
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
/** KiCad's own rotation: text extends right (0), up (90), left (180) or
|
|
69
|
+
* down (270) from the anchor, in schematic Y-down coordinates. */
|
|
70
|
+
rot: number;
|
|
71
|
+
/** A `global` label draws as a bordered flag whose tip sits on the wire
|
|
72
|
+
* end; a `local` one is bare text standing on its wire. Nets that leave a
|
|
73
|
+
* wired run through a stub carry global flags everywhere (a local and a
|
|
74
|
+
* global label of one name do not connect in KiCad); a net wired whole
|
|
75
|
+
* keeps a plain local name on the run. Unstated means local. */
|
|
76
|
+
kind?: 'local' | 'global';
|
|
77
|
+
/** Flag shape of a global label; unstated means passive (a plain box). */
|
|
78
|
+
shape?: LabelShape;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Font height, mm, of a group caption; bold so a subsystem title reads at
|
|
82
|
+
* arm's length on an A1/A2 sheet. */
|
|
83
|
+
export const CAPTION_SIZE = 3.5;
|
|
84
|
+
/** Font height, mm, of every net label. */
|
|
85
|
+
export const LABEL_SIZE = 1.27;
|
|
86
|
+
/** Stroke thickness, mm, of label text and the flag outline drawn with it;
|
|
87
|
+
* KiCad's default is size/8 ≈ 0.16, too faint against a coloured wire. */
|
|
88
|
+
export const LABEL_THICKNESS = 0.2;
|
|
89
|
+
/** Wire stroke width, mm; 0 would mean KiCad's default of 0.1524. */
|
|
90
|
+
export const WIRE_WIDTH = 0.254;
|
|
91
|
+
|
|
58
92
|
export interface PlacementModel {
|
|
59
93
|
projectName: string;
|
|
60
94
|
paper: string;
|
|
@@ -64,10 +98,12 @@ export interface PlacementModel {
|
|
|
64
98
|
symbols: EmitSymbol[];
|
|
65
99
|
wires: { x1: number; y1: number; x2: number; y2: number; net: string; index: number }[];
|
|
66
100
|
junctions: { x: number; y: number }[];
|
|
67
|
-
labels:
|
|
101
|
+
labels: EmitLabel[];
|
|
68
102
|
noConnects: { x: number; y: number }[];
|
|
69
103
|
rectangles: { x1: number; y1: number; x2: number; y2: number; stroke: 'solid' | 'dash'; name: string }[];
|
|
70
104
|
captions: { text: string; x: number; y: number; name: string }[];
|
|
105
|
+
/** Wire and label colour per net, RGB 0..255; nets absent here draw in the theme default. */
|
|
106
|
+
netColors?: Record<string, [number, number, number]>;
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
/**
|
|
@@ -147,11 +183,15 @@ export function emitSchematic(model: PlacementModel): string {
|
|
|
147
183
|
L.push(`\t(no_connect (at ${knum(n.x)} ${knum(n.y)}) (uuid ${q(id(`no_connect/${knum(n.x)},${knum(n.y)}`))}))`);
|
|
148
184
|
}
|
|
149
185
|
|
|
186
|
+
const colorOf = (net: string): string => {
|
|
187
|
+
const c = model.netColors?.[net];
|
|
188
|
+
return c ? ` (color ${c[0]} ${c[1]} ${c[2]} 1)` : '';
|
|
189
|
+
};
|
|
150
190
|
const wires = [...model.wires].sort((a, b) => a.net.localeCompare(b.net) || a.index - b.index);
|
|
151
191
|
for (const w of wires) {
|
|
152
192
|
L.push('\t(wire');
|
|
153
193
|
L.push(`\t\t(pts (xy ${knum(w.x1)} ${knum(w.y1)}) (xy ${knum(w.x2)} ${knum(w.y2)}))`);
|
|
154
|
-
L.push(
|
|
194
|
+
L.push(`\t\t(stroke (width ${knum(WIRE_WIDTH)}) (type default)${colorOf(w.net)})`);
|
|
155
195
|
L.push(`\t\t(uuid ${q(id(`wire/${w.net}/${w.index}`))})`);
|
|
156
196
|
L.push('\t)');
|
|
157
197
|
}
|
|
@@ -168,18 +208,36 @@ export function emitSchematic(model: PlacementModel): string {
|
|
|
168
208
|
const caps = [...model.captions].sort((a, b) => a.name.localeCompare(b.name));
|
|
169
209
|
for (const c of caps) {
|
|
170
210
|
L.push(`\t(text ${q(c.text)} (at ${knum(c.x)} ${knum(c.y)} 0)`);
|
|
171
|
-
L.push(
|
|
211
|
+
L.push(`\t\t(effects (font (size ${knum(CAPTION_SIZE)} ${knum(CAPTION_SIZE)}) bold) (justify left top))`);
|
|
172
212
|
L.push(`\t\t(uuid ${q(id(`caption/${c.name}`))})`);
|
|
173
213
|
L.push('\t)');
|
|
174
214
|
}
|
|
175
215
|
|
|
176
216
|
const labels = [...model.labels].sort((a, b) => a.name.localeCompare(b.name) || a.x - b.x || a.y - b.y);
|
|
177
217
|
for (const lb of labels) {
|
|
218
|
+
const uuid = q(id(`label/${lb.name}/${knum(lb.x)},${knum(lb.y)}`));
|
|
219
|
+
if (lb.kind === 'global') {
|
|
220
|
+
// eeschema's own encoding of a flag: the stored angle is the true one
|
|
221
|
+
// and the justification names the side of the anchor the text is on
|
|
222
|
+
// (left = extends right/up, right = extends left/down)
|
|
223
|
+
const rot = ((lb.rot % 360) + 360) % 360;
|
|
224
|
+
const justify = rot === 180 || rot === 270 ? 'right' : 'left';
|
|
225
|
+
L.push(`\t(global_label ${q(lb.name)} (shape ${lb.shape ?? 'passive'}) (at ${knum(lb.x)} ${knum(lb.y)} ${knum(rot)}) (fields_autoplaced yes)`);
|
|
226
|
+
L.push(`\t\t(effects (font (size ${knum(LABEL_SIZE)} ${knum(LABEL_SIZE)}) (thickness ${knum(LABEL_THICKNESS)})${colorOf(lb.name)}) (justify ${justify}))`);
|
|
227
|
+
L.push(`\t\t(uuid ${uuid})`);
|
|
228
|
+
L.push(`\t\t(property "Intersheetrefs" "\${INTERSHEET_REFS}" (at ${knum(lb.x)} ${knum(lb.y)} 0)`);
|
|
229
|
+
L.push(`\t\t\t(effects (font (size ${knum(LABEL_SIZE)} ${knum(LABEL_SIZE)})) hide)`);
|
|
230
|
+
L.push('\t\t)');
|
|
231
|
+
L.push('\t)');
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// a local label never draws upside down: 180/270 are drawn as 0/90 with
|
|
235
|
+
// the justification mirrored, which is how eeschema itself stores them
|
|
178
236
|
const justify = lb.rot === 180 || lb.rot === 270 ? 'right bottom' : 'left bottom';
|
|
179
237
|
const drawRot = lb.rot === 180 ? 0 : lb.rot === 270 ? 90 : lb.rot;
|
|
180
238
|
L.push(`\t(label ${q(lb.name)} (at ${knum(lb.x)} ${knum(lb.y)} ${knum(drawRot)})`);
|
|
181
|
-
L.push(`\t\t(effects (font (size
|
|
182
|
-
L.push(`\t\t(uuid ${
|
|
239
|
+
L.push(`\t\t(effects (font (size ${knum(LABEL_SIZE)} ${knum(LABEL_SIZE)}) (thickness ${knum(LABEL_THICKNESS)})${colorOf(lb.name)}) (justify ${justify}))`);
|
|
240
|
+
L.push(`\t\t(uuid ${uuid})`);
|
|
183
241
|
L.push('\t)');
|
|
184
242
|
}
|
|
185
243
|
|
|
@@ -196,16 +254,22 @@ export function emitSchematic(model: PlacementModel): string {
|
|
|
196
254
|
L.push('\t(symbol');
|
|
197
255
|
L.push(`\t\t(lib_id ${q(s.libId)})`);
|
|
198
256
|
L.push(`\t\t(at ${knum(s.at.x)} ${knum(s.at.y)} ${knum(s.at.rot)})`);
|
|
257
|
+
if (s.mirror) L.push(`\t\t(mirror ${s.mirror})`);
|
|
199
258
|
L.push(`\t\t(unit ${unit})`);
|
|
200
259
|
L.push('\t\t(exclude_from_sim no)');
|
|
201
260
|
L.push('\t\t(in_bom yes)');
|
|
202
261
|
L.push('\t\t(on_board yes)');
|
|
203
262
|
L.push('\t\t(dnp no)');
|
|
204
263
|
L.push(`\t\t(uuid ${q(sid)})`);
|
|
205
|
-
|
|
264
|
+
// KiCad adds the symbol's rotation to a property's angle when it draws
|
|
265
|
+
// the text, so a part turned 90 or 270 stores 90 to keep its reference
|
|
266
|
+
// and value reading horizontally (every rotated resistor in KiCad's own
|
|
267
|
+
// demos does). Storing 0 stood the bootstrap caps' values on end.
|
|
268
|
+
const fieldRot = s.at.rot % 180 === 90 ? 90 : 0;
|
|
269
|
+
L.push(`\t\t(property "Reference" ${q(s.ref)} (at ${knum(s.refAt.x)} ${knum(s.refAt.y)} ${fieldRot})`);
|
|
206
270
|
L.push(`\t\t\t(effects (font (size 1.27 1.27))${hideRef})`);
|
|
207
271
|
L.push('\t\t)');
|
|
208
|
-
L.push(`\t\t(property "Value" ${q(s.value)} (at ${knum(s.valueAt.x)} ${knum(s.valueAt.y)}
|
|
272
|
+
L.push(`\t\t(property "Value" ${q(s.value)} (at ${knum(s.valueAt.x)} ${knum(s.valueAt.y)} ${fieldRot})`);
|
|
209
273
|
L.push(`\t\t\t(effects (font (size 1.27 1.27))${hideValue})`);
|
|
210
274
|
L.push('\t\t)');
|
|
211
275
|
L.push(`\t\t(property "Footprint" ${q(s.footprint)} (at ${knum(s.at.x)} ${knum(s.at.y)} 0)`);
|
package/src/kicad/legibility.ts
CHANGED
|
@@ -183,12 +183,29 @@ function transformBounds(at: { x: number; y: number; rot: number }, mirror: 'x'
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/** Conservative text box, centered on the anchor; vertical when rotated 90/270. */
|
|
186
|
-
function textBounds(t: {
|
|
186
|
+
function textBounds(t: {
|
|
187
|
+
text: string;
|
|
188
|
+
x: number;
|
|
189
|
+
y: number;
|
|
190
|
+
rot: number;
|
|
191
|
+
height: number;
|
|
192
|
+
justifyH?: 'left' | 'right' | null;
|
|
193
|
+
justifyV?: 'top' | 'bottom' | null;
|
|
194
|
+
}): Bounds {
|
|
187
195
|
const w = Math.max(1, t.text.length) * TEXT_ADVANCE * t.height;
|
|
188
196
|
const h = t.height;
|
|
189
197
|
const vertical = Math.abs(t.rot % 180) === 90;
|
|
190
198
|
const [bw, bh] = vertical ? [h, w] : [w, h];
|
|
191
|
-
|
|
199
|
+
// A free text is centred on its anchor unless its effects justify it: the
|
|
200
|
+
// engine's group captions are `left top`, so their box runs right and down
|
|
201
|
+
// from the anchor. Measuring those as centred put half the caption's width
|
|
202
|
+
// left of the group box and flagged a caption 12 mm inside the frame as
|
|
203
|
+
// out of it (esp32-amp, A1 at full width).
|
|
204
|
+
const jh = vertical ? null : (t.justifyH ?? null);
|
|
205
|
+
const jv = vertical ? null : (t.justifyV ?? null);
|
|
206
|
+
const minX = jh === 'left' ? t.x : jh === 'right' ? t.x - bw : t.x - bw / 2;
|
|
207
|
+
const minY = jv === 'top' ? t.y : jv === 'bottom' ? t.y - bh : t.y - bh / 2;
|
|
208
|
+
return { minX, minY, maxX: minX + bw, maxY: minY + bh };
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
/**
|
|
@@ -210,19 +227,44 @@ function textBounds(t: { text: string; x: number; y: number; rot: number; height
|
|
|
210
227
|
* of them on the wrong side of the anchor either invents a collision with the
|
|
211
228
|
* symbol the label serves or hides a real one.
|
|
212
229
|
*/
|
|
213
|
-
function labelBounds(l: { name: string; x: number; y: number; rot: number; height: number; justify?: 'left' | 'right' | null }): Bounds {
|
|
230
|
+
function labelBounds(l: { name: string; x: number; y: number; rot: number; height: number; justify?: 'left' | 'right' | null; kind?: string }): Bounds {
|
|
214
231
|
const w = Math.max(1, l.name.length) * TEXT_ADVANCE * l.height;
|
|
215
232
|
const h = l.height;
|
|
216
233
|
const rot = ((l.rot % 360) + 360) % 360;
|
|
234
|
+
if (l.kind === 'global_label' || l.kind === 'hierarchical_label') {
|
|
235
|
+
// A flag is drawn two heights tall, CENTRED on the anchor line, with its
|
|
236
|
+
// outline (a margin each side of the text plus the pointed tip) running
|
|
237
|
+
// away from the anchor in the angle's direction; the stored justification
|
|
238
|
+
// only restates that direction. Measured from eeschema's own outline.
|
|
239
|
+
const fw = w + 2 * h;
|
|
240
|
+
if (rot === 90) return { minX: l.x - h, minY: l.y - fw, maxX: l.x + h, maxY: l.y };
|
|
241
|
+
if (rot === 270) return { minX: l.x - h, minY: l.y, maxX: l.x + h, maxY: l.y + fw };
|
|
242
|
+
if (rot === 180) return { minX: l.x - fw, minY: l.y - h, maxX: l.x, maxY: l.y + h };
|
|
243
|
+
return { minX: l.x, minY: l.y - h, maxX: l.x + fw, maxY: l.y + h };
|
|
244
|
+
}
|
|
217
245
|
const justified = l.justify ?? (rot === 180 || rot === 270 ? 'right' : 'left');
|
|
218
246
|
if (rot === 90 || rot === 270) {
|
|
219
247
|
return justified === 'left'
|
|
220
248
|
? { minX: l.x - h / 2, minY: l.y - w, maxX: l.x + h / 2, maxY: l.y }
|
|
221
249
|
: { minX: l.x - h / 2, minY: l.y, maxX: l.x + h / 2, maxY: l.y + w };
|
|
222
250
|
}
|
|
251
|
+
// Vertically the text STANDS on its anchor: eeschema draws a horizontal
|
|
252
|
+
// label bottom-justified, so the box runs one height above the anchor line
|
|
253
|
+
// and nothing below it. Measuring it centred put half the box below the
|
|
254
|
+
// wire the label sits on and called every label on a horizontal wire "text
|
|
255
|
+
// on a wire" — the ordinary way a person names a wire.
|
|
223
256
|
return justified === 'right'
|
|
224
|
-
? { minX: l.x - w, minY: l.y - h
|
|
225
|
-
: { minX: l.x, minY: l.y - h
|
|
257
|
+
? { minX: l.x - w, minY: l.y - h, maxX: l.x, maxY: l.y }
|
|
258
|
+
: { minX: l.x, minY: l.y - h, maxX: l.x + w, maxY: l.y };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Does the wire lie behind a flag's tip, along the direction the flag's text does NOT extend? */
|
|
262
|
+
function onPoleSide(w: WireSeg, l: { x: number; y: number; rot: number }): boolean {
|
|
263
|
+
const r = ((l.rot % 360) + 360) % 360;
|
|
264
|
+
if (r === 0) return Math.max(w.x1, w.x2) <= l.x + TOL;
|
|
265
|
+
if (r === 180) return Math.min(w.x1, w.x2) >= l.x - TOL;
|
|
266
|
+
if (r === 90) return Math.min(w.y1, w.y2) >= l.y - TOL;
|
|
267
|
+
return Math.max(w.y1, w.y2) <= l.y + TOL;
|
|
226
268
|
}
|
|
227
269
|
|
|
228
270
|
function segIntersectsBounds(s: WireSeg, b: Bounds): boolean {
|
|
@@ -397,7 +439,11 @@ function checkSheet(
|
|
|
397
439
|
const labelBoxes = sheet.labels.map((l) => ({
|
|
398
440
|
l,
|
|
399
441
|
box: labelBounds(l),
|
|
400
|
-
|
|
442
|
+
// a plain label stands above its wire, so every wire through the anchor
|
|
443
|
+
// is its attachment; a flag is drawn centred on the anchor line, so a
|
|
444
|
+
// wire continuing under the text runs through the flag — only the wire
|
|
445
|
+
// behind the tip (the pole side) attaches it
|
|
446
|
+
attached: sheet.wires.filter((w) => pointOnSeg(l.x, l.y, w) && (l.kind === 'label' || onPoleSide(w, l))),
|
|
401
447
|
}));
|
|
402
448
|
|
|
403
449
|
// --- groups (design C1): sheet rectangles with a caption in the top band ---
|
|
@@ -529,6 +575,9 @@ function checkSheet(
|
|
|
529
575
|
// --- label-orientation: rotated where a horizontal draw would collide with nothing ---
|
|
530
576
|
for (const lb of labelBoxes) {
|
|
531
577
|
if (Math.abs(lb.l.rot % 180) !== 90) continue;
|
|
578
|
+
// a flag continuing a vertical wire is oriented by that wire, not by
|
|
579
|
+
// taste: turning it horizontal would hang it sideways off the wire end
|
|
580
|
+
if (lb.l.kind !== 'label' && lb.attached.some((w) => Math.abs(w.x1 - w.x2) < TOL)) continue;
|
|
532
581
|
const horizontal = labelBounds({ ...lb.l, rot: 0 });
|
|
533
582
|
const collides =
|
|
534
583
|
realSyms.some((s) => s.body && boundsOverlap(horizontal, s.body)) ||
|
package/src/kicad/score.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readSheetGeometry, type SheetGeometry, type Bounds } from './sexp.js';
|
|
1
|
+
import { readSheetGeometry, pinsOfUnit, pinAbsolute, type SheetGeometry, type Bounds } from './sexp.js';
|
|
2
2
|
import { checkLegibility, type LegibilityReport } from './legibility.js';
|
|
3
3
|
import type { LegibilityUserConfig } from '../config.js';
|
|
4
4
|
|
|
@@ -25,12 +25,23 @@ export interface ScoreMetric {
|
|
|
25
25
|
export interface ScoreReport {
|
|
26
26
|
composite: number;
|
|
27
27
|
metrics: ScoreMetric[];
|
|
28
|
+
/**
|
|
29
|
+
* Wiring style, scored without the drafting standard's conventions. The
|
|
30
|
+
* composite above needs group boxes, captions and a title block, so a
|
|
31
|
+
* hand-drawn sheet that lacks them is capped at 40 whatever its craft.
|
|
32
|
+
* This block measures only how the parts connect — the thing a reader of
|
|
33
|
+
* any schematic judges first — and is never capped, so an engine draft
|
|
34
|
+
* and the human drawing of the same circuit compare on one scale.
|
|
35
|
+
*/
|
|
36
|
+
style: { composite: number; metrics: ScoreMetric[] };
|
|
28
37
|
/** Applied error-finding cap, or null. */
|
|
29
38
|
cap: { appliedAt: number; reason: string } | null;
|
|
30
39
|
legibility: { error: number; advisory: number };
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
const DEFAULT_WEIGHTS: Record<string, number> = {
|
|
43
|
+
'pin-attachment': 10,
|
|
44
|
+
'island-parts': 5,
|
|
34
45
|
'wire-crossings': 10,
|
|
35
46
|
'wire-bends': 5,
|
|
36
47
|
'wire-length': 5,
|
|
@@ -46,6 +57,23 @@ const DEFAULT_WEIGHTS: Record<string, number> = {
|
|
|
46
57
|
'pair-symmetry': 5,
|
|
47
58
|
};
|
|
48
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Weights of the convention-free style composite. Calibrated on the KiCad
|
|
62
|
+
* demo corpus (fifteen hand-drawn projects, 17 to 1138 parts): a person wires
|
|
63
|
+
* 90 to 100 % of two-pin parts straight to another part, leaves 0 to 2 % as
|
|
64
|
+
* label-only islands, draws 0.3 to 1.2 power symbols per part and, outside
|
|
65
|
+
* bus-heavy boards, under one label per part. Engine drafts before pin
|
|
66
|
+
* attachment measured 14 to 53 % attached and 9 to 13 % islands.
|
|
67
|
+
*/
|
|
68
|
+
const STYLE_WEIGHTS: Record<string, number> = {
|
|
69
|
+
'pin-attachment': 30,
|
|
70
|
+
'island-parts': 20,
|
|
71
|
+
'power-symbol-economy': 15,
|
|
72
|
+
'labels-per-part': 15,
|
|
73
|
+
'crossings-per-wire': 10,
|
|
74
|
+
'straight-wire-ratio': 10,
|
|
75
|
+
};
|
|
76
|
+
|
|
49
77
|
/** Composite is capped this far below the Tier A floor when errors exist. */
|
|
50
78
|
const DEFAULT_FLOOR = 85;
|
|
51
79
|
const ERROR_CAP = 40;
|
|
@@ -81,6 +109,124 @@ function properCrossing(a: Seg, b: Seg): boolean {
|
|
|
81
109
|
return vx > hx1 + 0.01 && vx < hx2 - 0.01 && hy > vy1 + 0.01 && hy < vy2 - 0.01;
|
|
82
110
|
}
|
|
83
111
|
|
|
112
|
+
/** Library prefixes whose multi-pin symbols are not ICs for the style metrics. */
|
|
113
|
+
const NOT_IC = /^(Connector|Switch|TestPoint|Mechanical|Jumper|Graphic|Device:Crystal|Device:Q_|Device:D_|Device:LED)/;
|
|
114
|
+
|
|
115
|
+
export interface WiringStyle {
|
|
116
|
+
parts: number;
|
|
117
|
+
ics: number;
|
|
118
|
+
twoPin: number;
|
|
119
|
+
powerSymbols: number;
|
|
120
|
+
labels: number;
|
|
121
|
+
/** two-pin parts with at least one pin wired straight to another part */
|
|
122
|
+
twoPinAttached: number;
|
|
123
|
+
/** two-pin parts whose every pin ends in a label or nothing */
|
|
124
|
+
twoPinIslands: number;
|
|
125
|
+
icPins: number;
|
|
126
|
+
icPinsWired: number;
|
|
127
|
+
icPinsLabelled: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* How the parts of a sheet set connect, by following wires from each placed
|
|
132
|
+
* pin: to another part's pin, to a power symbol, to a net label, or nowhere.
|
|
133
|
+
* Wire joins are endpoint-to-endpoint and endpoint-onto-segment (a T), the
|
|
134
|
+
* same two ways KiCad itself joins them.
|
|
135
|
+
*/
|
|
136
|
+
export function measureWiringStyle(sheets: SheetGeometry[]): WiringStyle {
|
|
137
|
+
const W: WiringStyle = { parts: 0, ics: 0, twoPin: 0, powerSymbols: 0, labels: 0, twoPinAttached: 0, twoPinIslands: 0, icPins: 0, icPinsWired: 0, icPinsLabelled: 0 };
|
|
138
|
+
const key = (x: number, y: number): string => `${Math.round(x * 100)},${Math.round(y * 100)}`;
|
|
139
|
+
for (const sh of sheets) {
|
|
140
|
+
const parent = new Map<string, string>();
|
|
141
|
+
const find = (k: string): string => {
|
|
142
|
+
let r = k;
|
|
143
|
+
while (parent.get(r) !== r) r = parent.get(r)!;
|
|
144
|
+
while (parent.get(k) !== r) {
|
|
145
|
+
const n = parent.get(k)!;
|
|
146
|
+
parent.set(k, r);
|
|
147
|
+
k = n;
|
|
148
|
+
}
|
|
149
|
+
return r;
|
|
150
|
+
};
|
|
151
|
+
const add = (k: string): void => {
|
|
152
|
+
if (!parent.has(k)) parent.set(k, k);
|
|
153
|
+
};
|
|
154
|
+
const union = (a: string, b: string): void => {
|
|
155
|
+
add(a);
|
|
156
|
+
add(b);
|
|
157
|
+
const ra = find(a);
|
|
158
|
+
const rb = find(b);
|
|
159
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
160
|
+
};
|
|
161
|
+
for (const w of sh.wires) union(key(w.x1, w.y1), key(w.x2, w.y2));
|
|
162
|
+
const placed = sh.symbols.map((s) => ({
|
|
163
|
+
s,
|
|
164
|
+
pins: pinsOfUnit(sh.libPins.get(s.libId) ?? [], s.unit).map((p) => pinAbsolute(s.at, s.mirror, p)),
|
|
165
|
+
}));
|
|
166
|
+
for (const { pins } of placed) for (const p of pins) add(key(p.x, p.y));
|
|
167
|
+
for (const l of sh.labels) add(key(l.x, l.y));
|
|
168
|
+
// a point on a segment's interior joins it (T junction); index segments by
|
|
169
|
+
// their fixed coordinate so this stays linear in practice
|
|
170
|
+
const horiz = new Map<number, Seg[]>();
|
|
171
|
+
const vert = new Map<number, Seg[]>();
|
|
172
|
+
for (const w of sh.wires) {
|
|
173
|
+
if (Math.abs(w.y1 - w.y2) < 0.01) horiz.set(Math.round(w.y1 * 100), [...(horiz.get(Math.round(w.y1 * 100)) ?? []), w]);
|
|
174
|
+
else if (Math.abs(w.x1 - w.x2) < 0.01) vert.set(Math.round(w.x1 * 100), [...(vert.get(Math.round(w.x1 * 100)) ?? []), w]);
|
|
175
|
+
}
|
|
176
|
+
for (const k of [...parent.keys()]) {
|
|
177
|
+
const [xs, ys] = k.split(',');
|
|
178
|
+
const x = Number(xs) / 100;
|
|
179
|
+
const y = Number(ys) / 100;
|
|
180
|
+
for (const w of horiz.get(Number(ys)) ?? []) {
|
|
181
|
+
if (x > Math.min(w.x1, w.x2) + 0.01 && x < Math.max(w.x1, w.x2) - 0.01) union(k, key(w.x1, w.y1));
|
|
182
|
+
}
|
|
183
|
+
for (const w of vert.get(Number(xs)) ?? []) {
|
|
184
|
+
if (y > Math.min(w.y1, w.y2) + 0.01 && y < Math.max(w.y1, w.y2) - 0.01) union(k, key(w.x1, w.y1));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const compOf = (x: number, y: number): string => find(key(x, y));
|
|
188
|
+
const labelComps = new Set(sh.labels.map((l) => compOf(l.x, l.y)));
|
|
189
|
+
const pinComps = new Map<string, { ref: string; power: boolean }[]>();
|
|
190
|
+
for (const { s, pins } of placed) {
|
|
191
|
+
for (const p of pins) {
|
|
192
|
+
const c = compOf(p.x, p.y);
|
|
193
|
+
pinComps.set(c, [...(pinComps.get(c) ?? []), { ref: s.ref, power: s.isPower }]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
W.labels += sh.labels.length;
|
|
197
|
+
for (const { s, pins } of placed) {
|
|
198
|
+
if (s.isPower) {
|
|
199
|
+
W.powerSymbols++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!pins.length) continue;
|
|
203
|
+
W.parts++;
|
|
204
|
+
const kinds = pins.map((p) => {
|
|
205
|
+
const c = compOf(p.x, p.y);
|
|
206
|
+
const others = (pinComps.get(c) ?? []).filter((o) => o.ref !== s.ref);
|
|
207
|
+
if (others.some((o) => !o.power)) return 'part';
|
|
208
|
+
if (others.some((o) => o.power)) return 'power';
|
|
209
|
+
if (labelComps.has(c)) return 'label';
|
|
210
|
+
return 'open';
|
|
211
|
+
});
|
|
212
|
+
if (pins.length === 2) {
|
|
213
|
+
W.twoPin++;
|
|
214
|
+
if (kinds.includes('part')) W.twoPinAttached++;
|
|
215
|
+
else if (kinds.every((k) => k === 'label' || k === 'open')) W.twoPinIslands++;
|
|
216
|
+
}
|
|
217
|
+
if (pins.length >= 3 && !NOT_IC.test(s.libId)) {
|
|
218
|
+
W.ics++;
|
|
219
|
+
for (const k of kinds) {
|
|
220
|
+
W.icPins++;
|
|
221
|
+
if (k === 'part') W.icPinsWired++;
|
|
222
|
+
else if (k === 'label') W.icPinsLabelled++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return W;
|
|
228
|
+
}
|
|
229
|
+
|
|
84
230
|
export interface ScoreOptions {
|
|
85
231
|
docsDir?: string | null;
|
|
86
232
|
config?: LegibilityUserConfig;
|
|
@@ -259,6 +405,13 @@ export function scoreFromGeometry(
|
|
|
259
405
|
}
|
|
260
406
|
const pairSymmetry = pairs ? symmetric / pairs : 1;
|
|
261
407
|
|
|
408
|
+
// wiring style: how the parts connect (see STYLE_WEIGHTS for the calibration)
|
|
409
|
+
const ws = measureWiringStyle(sheets);
|
|
410
|
+
const attachment = ws.twoPin ? ws.twoPinAttached / ws.twoPin : 1;
|
|
411
|
+
const islands = ws.twoPin ? ws.twoPinIslands / ws.twoPin : 0;
|
|
412
|
+
const powerPerPart = ws.parts ? ws.powerSymbols / ws.parts : 0;
|
|
413
|
+
const labelsPerPart = ws.parts ? ws.labels / ws.parts : 0;
|
|
414
|
+
|
|
262
415
|
const horizLabels = labels.length
|
|
263
416
|
? labels.filter((l) => Math.abs(l.rot % 180) !== 90).length / labels.length
|
|
264
417
|
: 1;
|
|
@@ -267,7 +420,15 @@ export function scoreFromGeometry(
|
|
|
267
420
|
|
|
268
421
|
// ---- normalize to [0,1] goodness and compose ----
|
|
269
422
|
const raw: Record<string, { raw: number; score: number }> = {
|
|
423
|
+
'pin-attachment': { raw: attachment, score: Math.min(1, attachment / 0.9) },
|
|
424
|
+
'island-parts': { raw: islands, score: Math.max(0, 1 - islands / 0.2) },
|
|
425
|
+
'power-symbol-economy': { raw: powerPerPart, score: powerPerPart <= 0.8 ? 1 : 0.8 / powerPerPart },
|
|
426
|
+
'labels-per-part': { raw: labelsPerPart, score: labelsPerPart <= 1 ? 1 : 1 / labelsPerPart },
|
|
270
427
|
'wire-crossings': { raw: crossings, score: 1 / (1 + crossings) },
|
|
428
|
+
// the absolute count above floors at zero on any large board; per wire it
|
|
429
|
+
// stays comparable across sizes (the 1138-part demo crosses 0.77 per wire,
|
|
430
|
+
// the 89-part one 0.03)
|
|
431
|
+
'crossings-per-wire': { raw: segs.length ? crossings / segs.length : 0, score: 1 / (1 + 10 * (segs.length ? crossings / segs.length : 0)) },
|
|
271
432
|
'wire-bends': { raw: bends, score: 1 / (1 + bends / 8) },
|
|
272
433
|
'wire-length': { raw: totalLen, score: 1 / (1 + totalLen / (4 * diag)) },
|
|
273
434
|
'label-to-wire-ratio': { raw: labelRatio, score: labelRatio <= 1.5 ? 1 : 1.5 / labelRatio },
|
|
@@ -281,15 +442,26 @@ export function scoreFromGeometry(
|
|
|
281
442
|
'whitespace-balance': { raw: centroidOff, score: Math.max(0, 1 - centroidOff) },
|
|
282
443
|
'pair-symmetry': { raw: pairSymmetry, score: pairSymmetry },
|
|
283
444
|
};
|
|
284
|
-
const totalWeight = Object.
|
|
285
|
-
const metrics: ScoreMetric[] = Object.entries(raw)
|
|
445
|
+
const totalWeight = Object.entries(weights).reduce((a, [k, v]) => a + (k in raw ? v : 0), 0) || 1;
|
|
446
|
+
const metrics: ScoreMetric[] = Object.entries(raw)
|
|
447
|
+
.filter(([name]) => name in weights)
|
|
448
|
+
.map(([name, m]) => ({
|
|
449
|
+
name,
|
|
450
|
+
raw: m.raw,
|
|
451
|
+
score: m.score,
|
|
452
|
+
weight: weights[name]!,
|
|
453
|
+
contribution: (100 * weights[name]! * m.score) / totalWeight,
|
|
454
|
+
}));
|
|
455
|
+
let composite = metrics.reduce((a, m) => a + m.contribution, 0);
|
|
456
|
+
const styleTotal = Object.values(STYLE_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
457
|
+
const styleMetrics: ScoreMetric[] = Object.entries(STYLE_WEIGHTS).map(([name, weight]) => ({
|
|
286
458
|
name,
|
|
287
|
-
raw:
|
|
288
|
-
score:
|
|
289
|
-
weight
|
|
290
|
-
contribution: (100 *
|
|
459
|
+
raw: raw[name]!.raw,
|
|
460
|
+
score: raw[name]!.score,
|
|
461
|
+
weight,
|
|
462
|
+
contribution: (100 * weight * raw[name]!.score) / styleTotal,
|
|
291
463
|
}));
|
|
292
|
-
|
|
464
|
+
const styleComposite = Math.round(styleMetrics.reduce((a, m) => a + m.contribution, 0) * 100) / 100;
|
|
293
465
|
|
|
294
466
|
let cap: ScoreReport['cap'] = null;
|
|
295
467
|
if (legibility.counts.error > 0) {
|
|
@@ -306,6 +478,7 @@ export function scoreFromGeometry(
|
|
|
306
478
|
return {
|
|
307
479
|
composite: Math.round(composite * 100) / 100,
|
|
308
480
|
metrics,
|
|
481
|
+
style: { composite: styleComposite, metrics: styleMetrics },
|
|
309
482
|
cap,
|
|
310
483
|
legibility: legibility.counts,
|
|
311
484
|
};
|
|
@@ -319,5 +492,11 @@ export function formatScore(report: ScoreReport): string {
|
|
|
319
492
|
` ${m.name}: raw ${Math.round(m.raw * 10000) / 10000}, score ${Math.round(m.score * 1000) / 1000}, weight ${m.weight}, contributes ${Math.round(m.contribution * 100) / 100}`,
|
|
320
493
|
);
|
|
321
494
|
}
|
|
495
|
+
if (report.style) {
|
|
496
|
+
lines.push(`wiring style: ${report.style.composite}/100 (convention-free, never capped)`);
|
|
497
|
+
for (const m of report.style.metrics) {
|
|
498
|
+
lines.push(` ${m.name}: raw ${Math.round(m.raw * 10000) / 10000}, score ${Math.round(m.score * 1000) / 1000}, weight ${m.weight}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
322
501
|
return lines.join('\n');
|
|
323
502
|
}
|
package/src/kicad/sexp.ts
CHANGED
|
@@ -335,6 +335,11 @@ export interface TextItem {
|
|
|
335
335
|
/** Font height in mm (KiCad `(size h w)`); 1.27 when unstated. */
|
|
336
336
|
height: number;
|
|
337
337
|
hidden: boolean;
|
|
338
|
+
/** `(justify …)` components, or null when unspecified: KiCad anchors a
|
|
339
|
+
* free text at its centre unless the effects say otherwise, and the
|
|
340
|
+
* engine's group captions say `left top`. */
|
|
341
|
+
justifyH: 'left' | 'right' | null;
|
|
342
|
+
justifyV: 'top' | 'bottom' | null;
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
export interface LabelItem {
|
|
@@ -400,10 +405,16 @@ function effectsOf(node: SexpNode[]): { height: number; hidden: boolean } {
|
|
|
400
405
|
const effects = child(node, 'effects');
|
|
401
406
|
const font = effects ? child(effects, 'font') : undefined;
|
|
402
407
|
const size = font ? child(font, 'size') : undefined;
|
|
403
|
-
// hidden: legacy bare `hide` atom,
|
|
408
|
+
// hidden: legacy bare `hide` atom, v9 `(hide yes)` inside effects, or
|
|
409
|
+
// KiCad 10's `(hide yes)` as a direct child of the property (a sheet
|
|
410
|
+
// re-saved by eeschema 10 flagged every hidden power reference as text
|
|
411
|
+
// on a wire until this was read)
|
|
404
412
|
const hideNode = effects ? child(effects, 'hide') : undefined;
|
|
413
|
+
const hideProp = child(node, 'hide');
|
|
405
414
|
const hidden =
|
|
406
|
-
(effects?.some((c) => c === 'hide') ?? false) ||
|
|
415
|
+
(effects?.some((c) => c === 'hide') ?? false) ||
|
|
416
|
+
(hideNode !== undefined && atomAt(hideNode, 1) !== 'no') ||
|
|
417
|
+
(hideProp !== undefined && atomAt(hideProp, 1) !== 'no');
|
|
407
418
|
return { height: size ? num(size, 1, 1.27) : 1.27, hidden };
|
|
408
419
|
}
|
|
409
420
|
|
|
@@ -483,7 +494,7 @@ function libBodyBounds(root: SexpNode[]): Map<string, Bounds | null> {
|
|
|
483
494
|
return map;
|
|
484
495
|
}
|
|
485
496
|
|
|
486
|
-
function textItemsOf(sym: SexpNode[]): TextItem[] {
|
|
497
|
+
function textItemsOf(sym: SexpNode[], symRot = 0): TextItem[] {
|
|
487
498
|
const out: TextItem[] = [];
|
|
488
499
|
for (const p of children(sym, 'property')) {
|
|
489
500
|
const key = atomAt(p, 1);
|
|
@@ -494,9 +505,18 @@ function textItemsOf(sym: SexpNode[]): TextItem[] {
|
|
|
494
505
|
text: atomAt(p, 2) ?? '',
|
|
495
506
|
x: num(at, 1),
|
|
496
507
|
y: num(at, 2),
|
|
497
|
-
|
|
508
|
+
// KiCad draws a property at its stored angle PLUS the symbol's: a
|
|
509
|
+
// resistor turned 90 stores 90 on its fields to read horizontally,
|
|
510
|
+
// and a field stored at 0 on that resistor stands on end. The
|
|
511
|
+
// checker measures what is drawn.
|
|
512
|
+
rot: (((num(at, 3) + symRot) % 360) + 360) % 360,
|
|
498
513
|
height: fx.height,
|
|
499
514
|
hidden: fx.hidden,
|
|
515
|
+
// property text keeps the centred measurement: its justification is
|
|
516
|
+
// relative to the symbol's own rotation and mirror, which the checker
|
|
517
|
+
// does not model, and the engine emits it centred
|
|
518
|
+
justifyH: null,
|
|
519
|
+
justifyV: null,
|
|
500
520
|
});
|
|
501
521
|
}
|
|
502
522
|
return out;
|
|
@@ -544,7 +564,18 @@ export async function readSheetGeometry(rootSch: string): Promise<SheetGeometry[
|
|
|
544
564
|
const texts: TextItem[] = children(sheet.root, 'text').map((t) => {
|
|
545
565
|
const at = child(t, 'at');
|
|
546
566
|
const fx = effectsOf(t);
|
|
547
|
-
|
|
567
|
+
const effects = child(t, 'effects');
|
|
568
|
+
const justify = effects ? child(effects, 'justify') : undefined;
|
|
569
|
+
return {
|
|
570
|
+
text: atomAt(t, 1) ?? '',
|
|
571
|
+
x: num(at, 1),
|
|
572
|
+
y: num(at, 2),
|
|
573
|
+
rot: num(at, 3),
|
|
574
|
+
height: fx.height,
|
|
575
|
+
hidden: fx.hidden,
|
|
576
|
+
justifyH: justify?.some((c) => c === 'right') ? 'right' : justify?.some((c) => c === 'left') ? 'left' : null,
|
|
577
|
+
justifyV: justify?.some((c) => c === 'bottom') ? 'bottom' : justify?.some((c) => c === 'top') ? 'top' : null,
|
|
578
|
+
};
|
|
548
579
|
});
|
|
549
580
|
const rectangles: RectItem[] = children(sheet.root, 'rectangle').map((r) => {
|
|
550
581
|
const s = child(r, 'start');
|
|
@@ -576,7 +607,7 @@ export async function readSheetGeometry(rootSch: string): Promise<SheetGeometry[
|
|
|
576
607
|
mirror,
|
|
577
608
|
isPower: isPowerSymbol(sym.libId, powerSyms),
|
|
578
609
|
unit: sym.unit,
|
|
579
|
-
props: textItemsOf(node),
|
|
610
|
+
props: textItemsOf(node, sym.at.rot),
|
|
580
611
|
})),
|
|
581
612
|
wires,
|
|
582
613
|
labels,
|