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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { knum } from '../emit.js';
|
|
1
|
+
import { knum, CAPTION_SIZE } from '../emit.js';
|
|
2
2
|
import { powerSymbolSource, pwrFlagSource } from './symsource.js';
|
|
3
3
|
/**
|
|
4
4
|
* The rule-based deterministic drafting engine (design D1/D2). All geometry is
|
|
@@ -10,20 +10,137 @@ import { powerSymbolSource, pwrFlagSource } from './symsource.js';
|
|
|
10
10
|
const U = 1.27;
|
|
11
11
|
/** Stub length from a pin to its label/power symbol, in grid units. */
|
|
12
12
|
const STUB = 2;
|
|
13
|
-
/** Cell margin around a symbol body (room for stubs, labels, text), in units
|
|
14
|
-
|
|
13
|
+
/** Cell margin around a symbol body (room for stubs, labels, text), in units,
|
|
14
|
+
* for the first draft; a measured round shrinks it to `MEASURED_MARGIN`. */
|
|
15
|
+
const BASE_MARGIN = 4;
|
|
16
|
+
/** Cell margin once the cell is sized from what it actually drew. */
|
|
17
|
+
const MEASURED_MARGIN = 2;
|
|
18
|
+
/** Vertical cell margin, units: rows are tighter than columns because a
|
|
19
|
+
* part's texts sit beside it, not above and below; a pin that faces up or
|
|
20
|
+
* down with a rail or ground on it reserves `POWER_REACH` instead. */
|
|
21
|
+
const VMARGIN = 2;
|
|
22
|
+
const POWER_REACH = 6;
|
|
23
|
+
/** Clear grid units kept beyond a cell's measured drawn extent. */
|
|
24
|
+
const MEASURE_PAD = 1;
|
|
25
|
+
/** Padding, grid units, a group box keeps around the label and field text it
|
|
26
|
+
* encloses, and the clearance kept between two such boxes. */
|
|
27
|
+
const BOX_PAD = 1;
|
|
28
|
+
/** Grid units between two drawn group boxes, after the boxes have grown to
|
|
29
|
+
* their text: the one distance a reader sees between blocks. Equal to
|
|
30
|
+
* GROUP_GAP, the gap the wrap fits between rects, so a fit on measured
|
|
31
|
+
* boxes is exact. */
|
|
32
|
+
const BOX_LINE_GAP = 4;
|
|
33
|
+
const BOX_GAP = 4;
|
|
15
34
|
/** Vertical gap between rows and horizontal channel between columns, units. */
|
|
16
|
-
const ROW_GAP =
|
|
17
|
-
const CHANNEL =
|
|
35
|
+
const ROW_GAP = 3;
|
|
36
|
+
const CHANNEL = 4;
|
|
18
37
|
/** Gap between group boxes, units. */
|
|
19
|
-
const GROUP_GAP =
|
|
20
|
-
/** Local nets up to this many endpoints may be wired (design D2)
|
|
38
|
+
const GROUP_GAP = 4;
|
|
39
|
+
/** Local nets up to this many endpoints may be wired (design D2); a larger
|
|
40
|
+
* cluster is narrowed to the endpoints bound to one anchor before routing. */
|
|
21
41
|
const MAX_WIRED_ENDPOINTS = 4;
|
|
42
|
+
/** Subsets of one cluster the wire pass tries to route before it gives up. */
|
|
43
|
+
const ROUTE_ATTEMPTS = 64;
|
|
44
|
+
/** Nodes the masonry wrap's deal search visits before it keeps the best deal found so far. */
|
|
45
|
+
const MASONRY_NODE_BUDGET = 200_000;
|
|
22
46
|
/** Wire-span budget in mm beyond which a net becomes labels. */
|
|
23
47
|
const MAX_WIRE_SPAN = 50.8;
|
|
24
48
|
/** Label text metrics, matching the legibility checker's conservative box. */
|
|
25
49
|
const LABEL_HEIGHT = 1.27;
|
|
26
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Advance the ENGINE gives text when it spaces groups, clears labels and
|
|
52
|
+
* fields of one another, and draws the box around a group. The checker's 0.6
|
|
53
|
+
* is tuned below KiCad's stroke font on purpose (design C3: never a false
|
|
54
|
+
* collision); measured on a plotted sheet the font advances about 0.72 of
|
|
55
|
+
* its height, so an engine that cleared text at 0.6 drew sheets where a rail
|
|
56
|
+
* name ran into the label on the next pin row while the checker called them
|
|
57
|
+
* clean. The engine is the side that must be conservative: it reserves 0.8.
|
|
58
|
+
*/
|
|
59
|
+
const LABEL_ADVANCE = 0.8;
|
|
60
|
+
const TEXT_RESERVE = LABEL_ADVANCE;
|
|
61
|
+
/** Advance of KiCad's stroke font for the upper-case pin names inside a
|
|
62
|
+
* body, per height: wider than the mixed-case reserve above. */
|
|
63
|
+
const NAME_ADVANCE = 1.0;
|
|
64
|
+
/** Air between any two texts, mm. Boxes that merely do not overlap still
|
|
65
|
+
* read as one smudge on paper (esp32-amp's "100k" sat 0.6 mm above
|
|
66
|
+
* "USB_OV"); every clearance check pads the candidate by this. */
|
|
67
|
+
const TEXT_PAD = 0.8;
|
|
68
|
+
const padBox = (b, p = TEXT_PAD) => ({ minX: b.minX - p, minY: b.minY - p, maxX: b.maxX + p, maxY: b.maxY + p });
|
|
69
|
+
/** `COPPERHEAD_DRAFT_TRACE=1` prints every placement decision the engine
|
|
70
|
+
* makes silently: what it claimed, what it skipped and why, what it refused. */
|
|
71
|
+
const trace = (msg) => {
|
|
72
|
+
if (process.env['COPPERHEAD_DRAFT_TRACE'] === '1')
|
|
73
|
+
console.error(`[draft] ${msg}`);
|
|
74
|
+
};
|
|
75
|
+
/** `labelTextBox` at the reserve advance: what the text takes on paper. */
|
|
76
|
+
const labelReserveBox = (name, x, y, rot, kind = 'local') => labelBoxAt(name, x, y, rot, kind, TEXT_RESERVE);
|
|
77
|
+
/**
|
|
78
|
+
* Along-axis length a global label's flag adds beyond its text: the margin
|
|
79
|
+
* either side of the text plus the pointed tip. Measured from eeschema's own
|
|
80
|
+
* outline at 1.27 mm (a 12-character name draws 17.0 mm long against about
|
|
81
|
+
* 14.5 of text), and kept a little generous because the advance estimate
|
|
82
|
+
* under-reads wide glyphs.
|
|
83
|
+
*/
|
|
84
|
+
const FLAG_PAD = 2.5 * LABEL_HEIGHT;
|
|
85
|
+
/** Grid units of the caption band at the top of a group box: the bold
|
|
86
|
+
* caption at `CAPTION_SIZE` plus clearance over the tallest thing a cell can
|
|
87
|
+
* carry above its body (a rail symbol on an upward stub with its value text). */
|
|
88
|
+
const CAPTION_BAND = 7;
|
|
89
|
+
/**
|
|
90
|
+
* The box a label occupies at `advance` per character. A local label is bare
|
|
91
|
+
* text standing on its anchor (see `labelTextBox`); a global label is a flag
|
|
92
|
+
* two text heights tall, centred on the anchor line, whose length runs away
|
|
93
|
+
* from the anchor in the rotation's direction — 0 right, 90 up, 180 left,
|
|
94
|
+
* 270 down in schematic Y-down coordinates, exactly as eeschema draws it.
|
|
95
|
+
*/
|
|
96
|
+
const labelBoxAt = (name, x, y, rot, kind, advance) => {
|
|
97
|
+
const textW = Math.max(1, name.length) * advance * LABEL_HEIGHT;
|
|
98
|
+
if (kind === 'global') {
|
|
99
|
+
const w = textW + FLAG_PAD;
|
|
100
|
+
const h = LABEL_HEIGHT;
|
|
101
|
+
const r = ((rot % 360) + 360) % 360;
|
|
102
|
+
if (r === 90)
|
|
103
|
+
return { minX: x - h, minY: y - w, maxX: x + h, maxY: y };
|
|
104
|
+
if (r === 270)
|
|
105
|
+
return { minX: x - h, minY: y, maxX: x + h, maxY: y + w };
|
|
106
|
+
if (r === 180)
|
|
107
|
+
return { minX: x - w, minY: y - h, maxX: x, maxY: y + h };
|
|
108
|
+
return { minX: x, minY: y - h, maxX: x + w, maxY: y + h };
|
|
109
|
+
}
|
|
110
|
+
// A plain label STANDS on its anchor line: the checker measures it that way
|
|
111
|
+
// (`labelBounds`), and so does the engine. The union box that also reached
|
|
112
|
+
// half a height below the line dated from the checker's centred days; kept,
|
|
113
|
+
// it made every name on a row collide with that row's own continuing wire
|
|
114
|
+
// (esp32-amp's UART_TXD could stand nowhere on its 9 mm run).
|
|
115
|
+
return rot === 180
|
|
116
|
+
? { minX: x - textW, minY: y - LABEL_HEIGHT, maxX: x, maxY: y }
|
|
117
|
+
: { minX: x, minY: y - LABEL_HEIGHT, maxX: x + textW, maxY: y };
|
|
118
|
+
};
|
|
119
|
+
/** KiCad's label rotation for text running outward along a stub direction. */
|
|
120
|
+
const rotOutward = (o) => (o.dx === -1 ? 180 : o.dy === -1 ? 90 : o.dy === 1 ? 270 : 0);
|
|
121
|
+
/**
|
|
122
|
+
* Flag shape for a global label at a pin, from the pin's electrical type:
|
|
123
|
+
* an output pin's net leaves through an output flag, an input's arrives
|
|
124
|
+
* through an input flag, a bidirectional or tri-state pin's points both
|
|
125
|
+
* ways, and everything else (passive parts, power, unspecified) gets the
|
|
126
|
+
* plain box. Cosmetic to KiCad's ERC, informative to a reader.
|
|
127
|
+
*/
|
|
128
|
+
export function flagShape(etype) {
|
|
129
|
+
switch (etype) {
|
|
130
|
+
case 'output':
|
|
131
|
+
case 'open_collector':
|
|
132
|
+
case 'open_emitter':
|
|
133
|
+
case 'power_out':
|
|
134
|
+
return 'output';
|
|
135
|
+
case 'input':
|
|
136
|
+
return 'input';
|
|
137
|
+
case 'bidirectional':
|
|
138
|
+
case 'tri_state':
|
|
139
|
+
return 'bidirectional';
|
|
140
|
+
default:
|
|
141
|
+
return 'passive';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
27
144
|
/** How far a colliding label may ride its stub outward, in grid units.
|
|
28
145
|
* Deep enough to carry a bottom-pin label past the routing channel that runs
|
|
29
146
|
* under its connector (#220 phase 2); rungs stay ordered nearest-first, so a
|
|
@@ -71,6 +188,8 @@ const PAPERS = [
|
|
|
71
188
|
];
|
|
72
189
|
const FRAME = 10;
|
|
73
190
|
const TITLE_STRIP = 30;
|
|
191
|
+
/** The title block's width along the bottom edge, as the checker measures it. */
|
|
192
|
+
const TITLE_BLOCK_W = 110;
|
|
74
193
|
/** Max pin-to-pin gap, grid units, for chaining a passive bank on one trunk
|
|
75
194
|
* (#233): wide enough for two-pin parts sitting in adjacent COLUMNS (cell
|
|
76
195
|
* width plus the channel, ~23 units), tight enough that a trunk never spans
|
|
@@ -248,6 +367,37 @@ export function splitBankRuns(line, canStub, canJoin) {
|
|
|
248
367
|
flush();
|
|
249
368
|
return runs;
|
|
250
369
|
}
|
|
370
|
+
/** `sym` mirrored left-for-right (KiCad's `(mirror y)`), then turned by `rot`. */
|
|
371
|
+
const transformSym = (sym, rot, mirror) => {
|
|
372
|
+
if (!mirror)
|
|
373
|
+
return rotatedSym(sym, rot);
|
|
374
|
+
const pins = sym.pins.map((p) => ({ ...p, x: -p.x, angle: (((180 - p.angle) % 360) + 360) % 360 }));
|
|
375
|
+
const b = sym.body ? bodyBoundsOf(sym) : null;
|
|
376
|
+
const body = b ? { minX: -b.maxX, maxX: -b.minX, minY: b.minY, maxY: b.maxY } : null;
|
|
377
|
+
return rotatedSym({ ...sym, pins, body }, rot);
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* The symbol as KiCad draws it at rotation `rot` (0, 90, 180, 270): pins,
|
|
381
|
+
* their direction angles and the body box all turned in symbol space, so
|
|
382
|
+
* every downstream helper (pinAt, outward, bodyBoundsOf) reads the drawn
|
|
383
|
+
* geometry without knowing the part was turned. Matches `pinAbsolute`:
|
|
384
|
+
* rx = x cos − y sin, ry = x sin + y cos.
|
|
385
|
+
*/
|
|
386
|
+
const rotatedSym = (sym, rot) => {
|
|
387
|
+
const r = ((rot % 360) + 360) % 360;
|
|
388
|
+
if (r === 0)
|
|
389
|
+
return sym;
|
|
390
|
+
const turn = (x, y) => r === 90 ? { x: -y, y: x } : r === 180 ? { x: -x, y: -y } : { x: y, y: -x };
|
|
391
|
+
const pins = sym.pins.map((p) => ({ ...p, ...turn(p.x, p.y), angle: (p.angle + r) % 360 }));
|
|
392
|
+
const b = sym.body ? bodyBoundsOf(sym) : null;
|
|
393
|
+
let body = null;
|
|
394
|
+
if (b) {
|
|
395
|
+
const c1 = turn(b.minX, b.minY);
|
|
396
|
+
const c2 = turn(b.maxX, b.maxY);
|
|
397
|
+
body = { minX: Math.min(c1.x, c2.x), maxX: Math.max(c1.x, c2.x), minY: Math.min(c1.y, c2.y), maxY: Math.max(c1.y, c2.y) };
|
|
398
|
+
}
|
|
399
|
+
return { ...sym, pins, body };
|
|
400
|
+
};
|
|
251
401
|
const bodyBoundsOf = (sym) => {
|
|
252
402
|
if (sym.body)
|
|
253
403
|
return sym.body;
|
|
@@ -257,6 +407,49 @@ const bodyBoundsOf = (sym) => {
|
|
|
257
407
|
return { minX: -U, minY: -U, maxX: U, maxY: U };
|
|
258
408
|
return { minX: Math.min(...xs), minY: Math.min(...ys), maxX: Math.max(...xs), maxY: Math.max(...ys) };
|
|
259
409
|
};
|
|
410
|
+
/**
|
|
411
|
+
* Wire and label colours by function: rails one colour, grounds another,
|
|
412
|
+
* and every FAMILY of signal nets — the nets sharing a prefix before the
|
|
413
|
+
* first underscore, when there are at least two of them (I2S_BCLK, I2S_DIN,
|
|
414
|
+
* I2S_LRCLK; SPI_…; BTN_…) — a colour of its own from a fixed palette, in
|
|
415
|
+
* family-name order so the same design colours the same way every time. A
|
|
416
|
+
* lone signal keeps the theme's default: colouring everything in a block
|
|
417
|
+
* one colour would say nothing.
|
|
418
|
+
*/
|
|
419
|
+
const FAMILY_PALETTE = [
|
|
420
|
+
[30, 90, 200],
|
|
421
|
+
[130, 50, 170],
|
|
422
|
+
[0, 130, 120],
|
|
423
|
+
[210, 120, 0],
|
|
424
|
+
[180, 30, 140],
|
|
425
|
+
[110, 130, 0],
|
|
426
|
+
[140, 80, 40],
|
|
427
|
+
[40, 60, 140],
|
|
428
|
+
];
|
|
429
|
+
const RAIL_COLOR = [170, 30, 30];
|
|
430
|
+
const GROUND_COLOR = [70, 70, 70];
|
|
431
|
+
function netColorsOf(nets, classes) {
|
|
432
|
+
const out = {};
|
|
433
|
+
const families = new Map();
|
|
434
|
+
for (const n of nets) {
|
|
435
|
+
const cls = classes.get(n.name)?.cls ?? 'signal';
|
|
436
|
+
if (cls === 'rail')
|
|
437
|
+
out[n.name] = RAIL_COLOR;
|
|
438
|
+
else if (cls === 'ground')
|
|
439
|
+
out[n.name] = GROUND_COLOR;
|
|
440
|
+
else {
|
|
441
|
+
const m = /^([A-Za-z0-9]+)_/.exec(n.name);
|
|
442
|
+
if (m)
|
|
443
|
+
families.set(m[1], [...(families.get(m[1]) ?? []), n.name]);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const named = [...families.entries()].filter(([, members]) => members.length >= 2).sort((a, b) => a[0].localeCompare(b[0]));
|
|
447
|
+
named.forEach(([, members], i) => {
|
|
448
|
+
for (const name of members)
|
|
449
|
+
out[name] = FAMILY_PALETTE[i % FAMILY_PALETTE.length];
|
|
450
|
+
});
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
260
453
|
/** Pin connection point in schematic space for a part placed at (x, y), rot 0. */
|
|
261
454
|
const pinAt = (p, pin) => ({ x: p.x + pin.x, y: p.y - pin.y });
|
|
262
455
|
/** Outward direction of a pin (away from the body), schematic space. */
|
|
@@ -315,13 +508,64 @@ const boundsOverlap = (a, b) => a.minX < b.maxX - 0.01 && a.maxX > b.minX + 0.01
|
|
|
315
508
|
* conservative metrics. Shared by the de-collision pass and the overlap report
|
|
316
509
|
* so "clear" means one thing in the engine.
|
|
317
510
|
*/
|
|
318
|
-
const labelTextBox = (name, x, y, rot) =>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
511
|
+
const labelTextBox = (name, x, y, rot, kind = 'local') =>
|
|
512
|
+
// A local label is bottom-justified on its anchor: on paper the text stands
|
|
513
|
+
// on the wire line and rises one full height above it, and the checker's
|
|
514
|
+
// `labelBounds` measures exactly that. A global label's flag is measured as
|
|
515
|
+
// eeschema draws it (`labelBoxAt`).
|
|
516
|
+
labelBoxAt(name, x, y, rot, kind, LABEL_ADVANCE);
|
|
517
|
+
/**
|
|
518
|
+
* Where a wired run's name may sit when it must be a global flag, in
|
|
519
|
+
* preference order: standing up from the top of a vertical trunk (the flag on
|
|
520
|
+
* a pole a reviewer expects), then every other wire end continuing its
|
|
521
|
+
* segment outward, then the interior grid points of each segment with the
|
|
522
|
+
* flag perpendicular to the wire, upward before downward. Every candidate
|
|
523
|
+
* lies on a wire of the run, so the name always attaches.
|
|
524
|
+
*/
|
|
525
|
+
export function wiredFlagCandidates(segs) {
|
|
526
|
+
const out = [];
|
|
527
|
+
const seen = new Set();
|
|
528
|
+
const push = (x, y, rot) => {
|
|
529
|
+
const k = `${pointKey(x, y)}/${rot}`;
|
|
530
|
+
if (seen.has(k))
|
|
531
|
+
return;
|
|
532
|
+
seen.add(k);
|
|
533
|
+
out.push({ x, y, rot });
|
|
534
|
+
};
|
|
535
|
+
const vertical = segs.filter((s) => sameCoord(s.x1, s.x2));
|
|
536
|
+
const horizontal = segs.filter((s) => !sameCoord(s.x1, s.x2));
|
|
537
|
+
// Horizontal first: a name reads along the row it names (the drafting
|
|
538
|
+
// standard keeps text horizontal), so the ends of horizontal segments
|
|
539
|
+
// continuing outward come before a flag standing up from a trunk top, and
|
|
540
|
+
// both before a flag perpendicular to a wire's interior.
|
|
541
|
+
const ends = segs.flatMap((s) => [{ x: s.x1, y: s.y1 }, { x: s.x2, y: s.y2 }]).sort((a, b) => a.y - b.y || a.x - b.x);
|
|
542
|
+
for (const p of ends) {
|
|
543
|
+
for (const s of horizontal) {
|
|
544
|
+
if (!sameCoord(s.y1, p.y))
|
|
545
|
+
continue;
|
|
546
|
+
if (sameCoord(p.x, Math.max(s.x1, s.x2)))
|
|
547
|
+
push(p.x, p.y, 0);
|
|
548
|
+
if (sameCoord(p.x, Math.min(s.x1, s.x2)))
|
|
549
|
+
push(p.x, p.y, 180);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const tops = vertical.map((s) => ({ x: s.x1, y: Math.min(s.y1, s.y2) })).sort((a, b) => a.y - b.y || a.x - b.x);
|
|
553
|
+
for (const t of tops)
|
|
554
|
+
push(t.x, t.y, 90);
|
|
555
|
+
for (const p of ends) {
|
|
556
|
+
for (const s of vertical) {
|
|
557
|
+
if (sameCoord(s.x1, p.x) && sameCoord(p.y, Math.max(s.y1, s.y2)))
|
|
558
|
+
push(p.x, p.y, 270);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const s of segs) {
|
|
562
|
+
for (const rot of sameCoord(s.x1, s.x2) ? [0, 180] : [90, 270]) {
|
|
563
|
+
for (const p of interiorGridPoints([s], U))
|
|
564
|
+
push(p.x, p.y, rot);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
325
569
|
/**
|
|
326
570
|
* Pairs of labels naming DIFFERENT nets whose text boxes overlap.
|
|
327
571
|
*
|
|
@@ -332,7 +576,7 @@ const labelTextBox = (name, x, y, rot) => {
|
|
|
332
576
|
*/
|
|
333
577
|
export function findLabelOverlaps(labels) {
|
|
334
578
|
const out = new Map();
|
|
335
|
-
const boxes = labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot));
|
|
579
|
+
const boxes = labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot, l.kind));
|
|
336
580
|
for (let i = 0; i < labels.length; i++) {
|
|
337
581
|
for (let j = i + 1; j < labels.length; j++) {
|
|
338
582
|
const a = labels[i];
|
|
@@ -366,7 +610,174 @@ const segCrossesBody = (x1, y1, x2, y2, b) => {
|
|
|
366
610
|
return y1 > b.minY + 0.01 && y1 < b.maxY - 0.01 && inX;
|
|
367
611
|
return inX && inY; // conservative for diagonals (the engine never draws them)
|
|
368
612
|
};
|
|
613
|
+
/**
|
|
614
|
+
* Draft, then check the drawn group boxes against one another: a box grows
|
|
615
|
+
* past its tiling estimate when the text it must hold (a flag on a hung
|
|
616
|
+
* part, a rail name at the edge) reaches further than the pin labels the
|
|
617
|
+
* estimate counted, and two boxes then intersect (usb-atmega-node's Status
|
|
618
|
+
* over IO Headers). Each overlap widens the right-hand group's left reserve
|
|
619
|
+
* by the overlap and the sheet is drafted again; placement is a pure
|
|
620
|
+
* function of the intent and these reserves, so the result stays
|
|
621
|
+
* deterministic. Three rounds bound the cost.
|
|
622
|
+
*/
|
|
369
623
|
export function draftSchematicPlacement(validated, projectName, today) {
|
|
624
|
+
const reserves = new Map();
|
|
625
|
+
let frameSlack = 0;
|
|
626
|
+
let wrapGap = 0;
|
|
627
|
+
// The squeeze. The first draft sizes every cell by rule (a margin a side, a
|
|
628
|
+
// power symbol's worth under every hung part, a label beside every pin) and
|
|
629
|
+
// draws into that room; most of the room stays empty. Each further round
|
|
630
|
+
// sizes the cells from what the previous round actually drew — body,
|
|
631
|
+
// wires, labels, power symbols, field text of the cell and of everything
|
|
632
|
+
// hung on it — plus a pad, and draws again. Rounds continue while the
|
|
633
|
+
// group boxes shrink and the engine's own gates hold (no merged nets, the
|
|
634
|
+
// label-overlap budget kept); the smallest passing sheet is the result.
|
|
635
|
+
// This is the step a drafter does by eye after placing: look, then pull
|
|
636
|
+
// things together.
|
|
637
|
+
let measured;
|
|
638
|
+
let best = null;
|
|
639
|
+
let squeezeRounds = 0;
|
|
640
|
+
let areaBefore = 0;
|
|
641
|
+
const boxArea = (rects) => rects.reduce((a, r) => a + (r.x2 - r.x1) * (r.y2 - r.y1), 0);
|
|
642
|
+
let retries = 0;
|
|
643
|
+
// The look. Before any wrap is fitted the engine can only estimate how far
|
|
644
|
+
// a group's labels reach past its cells, and it estimates generously: a
|
|
645
|
+
// label's width beside every pin that might carry one. Those reserves
|
|
646
|
+
// decide how many groups share a row or column, so a wrapped sheet is
|
|
647
|
+
// drafted once more with the reach every box was actually measured to
|
|
648
|
+
// take, and that draft is kept when it fits the same or a smaller sheet
|
|
649
|
+
// with the gates still holding.
|
|
650
|
+
let reachMeasured;
|
|
651
|
+
let look = 'not-yet';
|
|
652
|
+
let lookVerdict;
|
|
653
|
+
for (let round = 0; round < 16; round++) {
|
|
654
|
+
const { model, report, rects, measured: nextMeasured, reach, wrapped } = draftOnce(validated, projectName, today, reserves, frameSlack, measured, wrapGap, reachMeasured);
|
|
655
|
+
let widened = false;
|
|
656
|
+
const stillWrong = [];
|
|
657
|
+
// A row or column wrapped to the full usable width leaves no room for the
|
|
658
|
+
// text its boxes grow to hold afterwards, and a box then crosses the
|
|
659
|
+
// frame (usb-atmega-node's IO Headers on A3). Any box past the frame
|
|
660
|
+
// shrinks the usable frame by the overflow and the sheet is drafted again.
|
|
661
|
+
const paper = PAPERS.find((p) => p.name === report.sheetFit.paper);
|
|
662
|
+
if (paper) {
|
|
663
|
+
const over = Math.max(0, ...rects.flatMap((r) => [FRAME - r.x1, r.x2 - (paper.w - FRAME), FRAME - r.y1, r.y2 - (paper.h - FRAME)]));
|
|
664
|
+
if (over > 0.01) {
|
|
665
|
+
frameSlack += over + U;
|
|
666
|
+
widened = true;
|
|
667
|
+
stillWrong.push(`a box crosses the frame by ${over.toFixed(1)} mm`);
|
|
668
|
+
trace(`a group box crosses the frame by ${over.toFixed(2)} mm; the usable frame shrinks by ${frameSlack.toFixed(2)} mm and the sheet is drafted again`);
|
|
669
|
+
}
|
|
670
|
+
// A sheet fitted into the title strip was judged on its group rects
|
|
671
|
+
// before the boxes grew to their text; a box that grew into the title
|
|
672
|
+
// block's own corner (as the checker reserves it, narrowed on small
|
|
673
|
+
// pages) overflows the usable height by how far it reaches in.
|
|
674
|
+
const cornerX = paper.w - FRAME - Math.min(TITLE_BLOCK_W, (paper.w - 2 * FRAME) / 2);
|
|
675
|
+
const cornerY = paper.h - FRAME - Math.min(TITLE_STRIP, (paper.h - 2 * FRAME) / 4);
|
|
676
|
+
const into = Math.max(0, ...rects.map((r) => (r.x2 > cornerX + 0.01 && r.y2 > cornerY + 0.01 ? r.y2 - cornerY : 0)));
|
|
677
|
+
if (over <= 0.01 && into > 0.01) {
|
|
678
|
+
frameSlack += into + U;
|
|
679
|
+
widened = true;
|
|
680
|
+
stillWrong.push(`a box enters the title block's corner by ${into.toFixed(1)} mm`);
|
|
681
|
+
trace(`a group box enters the title block's corner by ${into.toFixed(2)} mm; the usable frame shrinks by ${frameSlack.toFixed(2)} mm and the sheet is drafted again`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (let i = 0; i < rects.length; i++) {
|
|
685
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
686
|
+
const a = rects[i];
|
|
687
|
+
const b = rects[j];
|
|
688
|
+
const ox = Math.min(a.x2, b.x2) - Math.max(a.x1, b.x1);
|
|
689
|
+
const oy = Math.min(a.y2, b.y2) - Math.max(a.y1, b.y1);
|
|
690
|
+
if (ox <= 0.01 || oy <= 0.01)
|
|
691
|
+
continue;
|
|
692
|
+
widened = true;
|
|
693
|
+
stillWrong.push(`"${a.name}" and "${b.name}" intersect`);
|
|
694
|
+
if (oy < ox) {
|
|
695
|
+
// boxes in different rows (or columns) touching along the wrap
|
|
696
|
+
// gap: widen the gap between wrapped rows and columns
|
|
697
|
+
wrapGap += oy + U;
|
|
698
|
+
trace(`group boxes "${a.name}" and "${b.name}" overlap by ${oy.toFixed(2)} mm across the wrap; the wrap gap grows to ${wrapGap.toFixed(2)} mm and the sheet is drafted again`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const right = a.x1 < b.x1 ? b : a;
|
|
702
|
+
const r = reserves.get(right.name) ?? { left: 0, right: 0 };
|
|
703
|
+
r.left += ox + U;
|
|
704
|
+
reserves.set(right.name, r);
|
|
705
|
+
trace(`group boxes "${a.name}" and "${b.name}" overlap by ${ox.toFixed(2)} mm; "${right.name}" reserves ${r.left.toFixed(2)} mm more on its left and the sheet is drafted again`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (widened && retries < 3) {
|
|
709
|
+
retries++;
|
|
710
|
+
continue; // the same cells, re-tiled: not a squeeze round
|
|
711
|
+
}
|
|
712
|
+
const area = boxArea(rects);
|
|
713
|
+
const refusals = report.notes.filter((n) => /refused/.test(n)).length;
|
|
714
|
+
const paperIdx = PAPERS.findIndex((p) => p.name === report.paper);
|
|
715
|
+
// a round whose boxes still overlap or cross the frame after three
|
|
716
|
+
// re-tilings is not a sheet to keep; nor one that lost a hung part, or
|
|
717
|
+
// needs a larger sheet, or shrank by less than three percent
|
|
718
|
+
const passes = !widened && report.mergedNets.length === 0 && !report.labelOverlapBudgetExceeded;
|
|
719
|
+
if (widened)
|
|
720
|
+
report.notes.push(`group boxes still wrong after ${retries} re-tilings (${stillWrong.join('; ')}); the sheet is drawn as it stands`);
|
|
721
|
+
retries = 0;
|
|
722
|
+
reserves.clear();
|
|
723
|
+
frameSlack = 0;
|
|
724
|
+
wrapGap = 0;
|
|
725
|
+
if (!best) {
|
|
726
|
+
areaBefore = area;
|
|
727
|
+
best = { model, report, area, refusals, paperIdx };
|
|
728
|
+
if (look === 'not-yet' && wrapped && passes) {
|
|
729
|
+
look = 'drafting';
|
|
730
|
+
reachMeasured = reach;
|
|
731
|
+
trace(`box reach measured: ${[...reach].map(([g, r]) => `${g.slice(0, 2).trim()} ${r.left.toFixed(1)}/${r.right.toFixed(1)} ${r.top.toFixed(1)}/${r.bottom.toFixed(1)}`).join(', ')}; the wrap is fitted again on it`);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
else if (look === 'drafting') {
|
|
736
|
+
look = 'done';
|
|
737
|
+
if (passes && refusals <= best.refusals && (paperIdx < best.paperIdx || (paperIdx === best.paperIdx && area <= best.area))) {
|
|
738
|
+
trace(`measured-reach draft kept: ${report.paper}, boxes ${area.toFixed(0)} mm² (was ${best.report.paper}, ${best.area.toFixed(0)} mm²)`);
|
|
739
|
+
lookVerdict = { kept: true, paperBefore: best.report.paper, paperAfter: report.paper };
|
|
740
|
+
if (paperIdx < best.paperIdx)
|
|
741
|
+
report.notes.push(`label reach measured: the wrap fitted again takes ${report.paper}, not ${best.report.paper}`);
|
|
742
|
+
areaBefore = area;
|
|
743
|
+
best = { model, report, area, refusals, paperIdx };
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
trace(`measured-reach draft not kept: ${passes ? `${report.paper}, boxes ${area.toFixed(0)} mm²` : 'gates failed'} against ${best.report.paper}, ${best.area.toFixed(0)} mm²`);
|
|
747
|
+
lookVerdict = { kept: false, paperBefore: best.report.paper, paperAfter: best.report.paper };
|
|
748
|
+
reachMeasured = undefined;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
else if (passes && refusals <= best.refusals && paperIdx <= best.paperIdx && area < best.area * 0.97) {
|
|
752
|
+
best = { model, report, area, refusals, paperIdx };
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
trace(`squeeze round ${squeezeRounds}: ${passes ? `boxes ${area.toFixed(0)} mm² did not shrink` : 'gates failed'}; keeping the previous round`);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
// Rounds beyond the first run only when asked for: on the boards drafted
|
|
759
|
+
// so far the rules were within a few percent of the drawing, and the
|
|
760
|
+
// extra drafts cost time and re-pin every fixture. The measurement and
|
|
761
|
+
// the report are always made.
|
|
762
|
+
if (squeezeRounds >= 3 || process.env['COPPERHEAD_DRAFT_SQUEEZE'] !== '1')
|
|
763
|
+
break;
|
|
764
|
+
measured = nextMeasured;
|
|
765
|
+
squeezeRounds++;
|
|
766
|
+
trace(`squeeze round ${squeezeRounds}: cells measured, group boxes ${area.toFixed(0)} mm²; drafting again`);
|
|
767
|
+
}
|
|
768
|
+
const out = best;
|
|
769
|
+
out.report.sheetFit.squeeze = { rounds: squeezeRounds, boxAreaBefore: Math.round(areaBefore), boxAreaAfter: Math.round(out.area) };
|
|
770
|
+
if (lookVerdict)
|
|
771
|
+
out.report.sheetFit.look = lookVerdict;
|
|
772
|
+
if (squeezeRounds > 0) {
|
|
773
|
+
out.report.notes.push(`cells measured: group boxes ${Math.round(areaBefore / 1000)} k mm² to ${Math.round(out.area / 1000)} k mm² over ${squeezeRounds} round(s) on ${out.report.paper}`);
|
|
774
|
+
}
|
|
775
|
+
return { model: out.model, report: out.report };
|
|
776
|
+
}
|
|
777
|
+
function draftOnce(validated, projectName, today, reserves, frameSlack = 0, measured, wrapGap = 0, reachMeasured) {
|
|
778
|
+
// a measured round keeps only a small margin: the measured overhang says
|
|
779
|
+
// where the drawing actually reaches
|
|
780
|
+
const MARGIN = measured ? MEASURED_MARGIN : BASE_MARGIN;
|
|
370
781
|
const { intent, symbols, docGroups } = validated;
|
|
371
782
|
const notes = [];
|
|
372
783
|
// ---------- net classification (deterministic, visible in the report) ----------
|
|
@@ -410,6 +821,19 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
410
821
|
}));
|
|
411
822
|
});
|
|
412
823
|
const instByKey = new Map(instances.map((i) => [i.key, i]));
|
|
824
|
+
/** The pin that controls a transistor (base or gate), or null for anything else. */
|
|
825
|
+
const controlPinOf = (key) => {
|
|
826
|
+
const inst = instByKey.get(key);
|
|
827
|
+
if (!inst || inst.sym.pins.length !== 3)
|
|
828
|
+
return null;
|
|
829
|
+
if (!/^Transistor|^Device:Q_|^Q\d/.test(inst.part.libId) && !/^Q\d/.test(inst.ref))
|
|
830
|
+
return null;
|
|
831
|
+
return inst.sym.pins.find((p) => /^(B|G|Base|Gate)$/i.test(p.name)) ?? null;
|
|
832
|
+
};
|
|
833
|
+
const isTransistor = (key) => controlPinOf(key) !== null;
|
|
834
|
+
/** Switches are the anchors of a button block: the pull-up, the debounce
|
|
835
|
+
* cap and the ESD diode all hang on the switch's signal pin. */
|
|
836
|
+
const isSwitch = (key) => /^Switch[:_]|^Device:SW_|^Button/i.test(instByKey.get(key)?.part.libId ?? '');
|
|
413
837
|
/** Placed instances per refdes, for expanding a common pin's endpoint. */
|
|
414
838
|
const instancesOfRef = new Map();
|
|
415
839
|
for (const inst of instances)
|
|
@@ -509,14 +933,45 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
509
933
|
let right = 0;
|
|
510
934
|
for (const key of keys) {
|
|
511
935
|
const inst = instByKey.get(key);
|
|
936
|
+
if (!inst)
|
|
937
|
+
continue;
|
|
938
|
+
// a part with pins only on its top and bottom (a vertical R or C)
|
|
939
|
+
// carries its reference and value beside it on the right
|
|
940
|
+
if (inst.sym.pins.every((pin) => outward(pin).dx === 0)) {
|
|
941
|
+
const fieldW = Math.max(inst.ref.length + 1, inst.part.value.length) * TEXT_RESERVE * LABEL_HEIGHT;
|
|
942
|
+
right = Math.max(right, 1.27 + fieldW + 1.27);
|
|
943
|
+
}
|
|
512
944
|
for (const pin of inst?.sym.pins ?? []) {
|
|
513
|
-
const net = signalNetOfPin.get(`${inst.ref}.${pin.number}`);
|
|
514
|
-
if (!net)
|
|
515
|
-
continue;
|
|
516
945
|
const o = outward(pin);
|
|
517
946
|
if (o.dx === 0)
|
|
518
947
|
continue;
|
|
519
|
-
const
|
|
948
|
+
const ep = `${inst.ref}.${pin.number}`;
|
|
949
|
+
const signal = signalNetOfPin.get(ep);
|
|
950
|
+
const power = signal ? undefined : netByEndpoint.get(ep);
|
|
951
|
+
if (!signal && !power)
|
|
952
|
+
continue;
|
|
953
|
+
// a small signal net whose every endpoint is in this group is wired,
|
|
954
|
+
// not labelled: no flag reaches out from it (counting one for every
|
|
955
|
+
// pin put 27 mm between groups whose facing sides carry no label). A
|
|
956
|
+
// larger in-group net may still end in stub labels, so it keeps its
|
|
957
|
+
// reserve.
|
|
958
|
+
if (signal) {
|
|
959
|
+
const net = netByEndpoint.get(ep);
|
|
960
|
+
const others = (net?.pins ?? []).filter((other) => other !== ep).map((other) => /^([^.]+)\./.exec(other)?.[1] ?? '');
|
|
961
|
+
const leaves = !net || net.pins.length > MAX_WIRED_ENDPOINTS || others.some((ref) => partByRef.get(ref)?.group !== inst.part.group);
|
|
962
|
+
// only when every other endpoint is a two-lead part (or a test
|
|
963
|
+
// point): those hang on the pin and are wired by construction; two
|
|
964
|
+
// ICs of one group may still sit past wire span and take labels
|
|
965
|
+
const hangs = others.every((ref) => (symbols.get(ref)?.pins.length ?? 3) <= 2);
|
|
966
|
+
if (!leaves && hangs)
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
// a signal: stub plus the label's flag (text, margins and tip); a
|
|
970
|
+
// rail or ground on a sideways pin: the longer power stub, the bar,
|
|
971
|
+
// and the name drawn outward in line
|
|
972
|
+
const extent = signal
|
|
973
|
+
? STUB * U + Math.max(1, signal.length) * TEXT_RESERVE * LABEL_HEIGHT + FLAG_PAD
|
|
974
|
+
: (STUB + 2) * U + 1.905 + Math.max(1, power.name.length) * TEXT_RESERVE * LABEL_HEIGHT;
|
|
520
975
|
if (o.dx === -1)
|
|
521
976
|
left = Math.max(left, extent);
|
|
522
977
|
else
|
|
@@ -526,8 +981,9 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
526
981
|
return { left, right };
|
|
527
982
|
};
|
|
528
983
|
/** Extra gap units so facing label text fits a boundary whose body-to-body
|
|
529
|
-
* clearance is `baseUnits
|
|
530
|
-
|
|
984
|
+
* clearance is `baseUnits`: the two texts, the box padding each group draws
|
|
985
|
+
* around its own text (`BOX_PAD` a side), and the gap between the boxes. */
|
|
986
|
+
const widenBy = (rightOfPrev, leftOfNext, baseUnits) => Math.max(0, ceilU(rightOfPrev + leftOfNext) + 2 * BOX_PAD + BOX_GAP - baseUnits);
|
|
531
987
|
// ---------- group ordering: hints, then SUBSYSTEMS.md order, then name ----------
|
|
532
988
|
const groupNames = [...new Set(intent.parts.filter((p) => !symbols.get(p.ref).isPower).map((p) => p.group))];
|
|
533
989
|
const orderIndex = (g) => {
|
|
@@ -544,9 +1000,20 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
544
1000
|
const placed = new Map();
|
|
545
1001
|
const groupRects = [];
|
|
546
1002
|
const groupOf = new Map();
|
|
1003
|
+
/** Part key -> the anchor it was hung on or laid beside. A bound part is
|
|
1004
|
+
* wired to its anchor whatever the distance: the shelf placed it there
|
|
1005
|
+
* deliberately, and a lane past the wire span still belongs to its pin. */
|
|
1006
|
+
const boundTo = new Map();
|
|
547
1007
|
const groupExtents = new Map();
|
|
548
1008
|
for (const gname of groupNames) {
|
|
549
|
-
|
|
1009
|
+
// The estimate reserves a label's width beside every pin that might take
|
|
1010
|
+
// one; the drawing seldom uses a tenth of it. A round that has seen the
|
|
1011
|
+
// drawing reserves what its box actually grew past its cells, plus a
|
|
1012
|
+
// unit.
|
|
1013
|
+
const m = reachMeasured?.get(gname);
|
|
1014
|
+
const e = m ? { left: m.left + U, right: m.right + U } : labelExtents(instances.filter((i) => i.part.group === gname && !i.sym.isPower).map((i) => i.key));
|
|
1015
|
+
const r = reserves.get(gname);
|
|
1016
|
+
groupExtents.set(gname, { left: e.left + (r?.left ?? 0), right: e.right + (r?.right ?? 0) });
|
|
550
1017
|
}
|
|
551
1018
|
/**
|
|
552
1019
|
* Place every group's cells. `bandBudgetW` caps a single group's width, in
|
|
@@ -594,23 +1061,83 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
594
1061
|
}
|
|
595
1062
|
}
|
|
596
1063
|
}
|
|
597
|
-
|
|
598
|
-
|
|
1064
|
+
// The IC leads its group. Edges above are stored refdes-ordered, and
|
|
1065
|
+
// C, D, J, L, Q and R all sort before U, so plain propagation pushed
|
|
1066
|
+
// every IC to the deepest column on the right with its passives strung
|
|
1067
|
+
// out to its left — a reader looked for the part the group is about and
|
|
1068
|
+
// found it last. When a group has ICs (three or more pins, not a
|
|
1069
|
+
// connector), they anchor the first column after the connectors and
|
|
1070
|
+
// every other member sits at its signal-net hop distance from the
|
|
1071
|
+
// nearest IC; a member no signal path reaches stays beside the ICs.
|
|
1072
|
+
// A group with no IC keeps the edge propagation, where the connector
|
|
1073
|
+
// column is the only anchor there is.
|
|
1074
|
+
// A transistor is a three-pin part, but it is not what a group is
|
|
1075
|
+
// about: it belongs at the end of the run that drives its base, the way
|
|
1076
|
+
// a hand-drawn sheet puts a level shifter or a switch right on the pin.
|
|
1077
|
+
const groupIC = (key) => instByKey.get(key).sym.pins.length >= 3 && !isConnector(instByKey.get(key).part) && !isTransistor(key);
|
|
1078
|
+
const anchors = members.filter((m) => groupIC(m.key)).map((m) => m.key);
|
|
1079
|
+
if (anchors.length) {
|
|
1080
|
+
// IC to IC, the signal flow still propagates (a chain of forty
|
|
1081
|
+
// stages is forty columns, not one), refdes-ordered as before
|
|
1082
|
+
const icSet = new Set(anchors);
|
|
1083
|
+
for (let iter = 0; iter < anchors.length; iter++) {
|
|
1084
|
+
let changed = false;
|
|
1085
|
+
for (const e of edges) {
|
|
1086
|
+
if (!icSet.has(e.from) || !icSet.has(e.to))
|
|
1087
|
+
continue;
|
|
1088
|
+
const want = depth.get(e.from) + 1;
|
|
1089
|
+
if (depth.get(e.to) < want) {
|
|
1090
|
+
depth.set(e.to, want);
|
|
1091
|
+
changed = true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (!changed)
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
// then every other member at its hop distance past the nearest IC,
|
|
1098
|
+
// never routing through a connector or another IC
|
|
1099
|
+
const adj = new Map();
|
|
599
1100
|
for (const e of edges) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1101
|
+
adj.set(e.from, [...(adj.get(e.from) ?? []), e.to]);
|
|
1102
|
+
adj.set(e.to, [...(adj.get(e.to) ?? []), e.from]);
|
|
1103
|
+
}
|
|
1104
|
+
const dist = new Map(anchors.map((k) => [k, depth.get(k)]));
|
|
1105
|
+
const queue = [...anchors].sort((a, b) => dist.get(a) - dist.get(b));
|
|
1106
|
+
while (queue.length) {
|
|
1107
|
+
const k = queue.shift();
|
|
1108
|
+
for (const o of adj.get(k) ?? []) {
|
|
1109
|
+
if (dist.has(o) || isConnector(instByKey.get(o).part))
|
|
1110
|
+
continue;
|
|
1111
|
+
dist.set(o, dist.get(k) + 1);
|
|
1112
|
+
queue.push(o);
|
|
604
1113
|
}
|
|
605
1114
|
}
|
|
606
|
-
|
|
607
|
-
|
|
1115
|
+
for (const [k, d] of dist)
|
|
1116
|
+
if (!icSet.has(k))
|
|
1117
|
+
depth.set(k, d);
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
for (let iter = 0; iter < members.length; iter++) {
|
|
1121
|
+
let changed = false;
|
|
1122
|
+
for (const e of edges) {
|
|
1123
|
+
const want = (depth.get(e.from) ?? 0) + 1;
|
|
1124
|
+
if ((depth.get(e.to) ?? 0) < want && want <= members.length) {
|
|
1125
|
+
depth.set(e.to, want);
|
|
1126
|
+
changed = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (!changed)
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
608
1132
|
}
|
|
609
1133
|
const depths = [...new Set([...depth.values()])].sort((a, b) => a - b);
|
|
610
1134
|
const columns = depths.map((d) => members.filter((m) => depth.get(m.key) === d).map((m) => m.key));
|
|
611
|
-
// barycenter row ordering (two sweeps), refdes as the deterministic
|
|
1135
|
+
// barycenter row ordering (two sweeps), refdes as the deterministic
|
|
1136
|
+
// tie; an IC stays at the top of its column so the group reads from
|
|
1137
|
+
// its top-left corner
|
|
612
1138
|
const rowOf = new Map();
|
|
613
|
-
|
|
1139
|
+
const lead = (ref) => (groupIC(ref) ? 0 : 1);
|
|
1140
|
+
columns.forEach((col) => col.sort((a, b) => lead(a) - lead(b) || a.localeCompare(b, undefined, { numeric: true })).forEach((r, i) => rowOf.set(r, i)));
|
|
614
1141
|
for (let sweep = 0; sweep < 2; sweep++) {
|
|
615
1142
|
for (let ci = 1; ci < columns.length; ci++) {
|
|
616
1143
|
const col = columns[ci];
|
|
@@ -623,33 +1150,760 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
623
1150
|
return rowOf.get(ref);
|
|
624
1151
|
return neigh.reduce((s, o) => s + rowOf.get(o), 0) / neigh.length;
|
|
625
1152
|
};
|
|
626
|
-
col.sort((a, b) => bary(a) - bary(b) || a.localeCompare(b, undefined, { numeric: true })).forEach((r, i) => rowOf.set(r, i));
|
|
1153
|
+
col.sort((a, b) => lead(a) - lead(b) || bary(a) - bary(b) || a.localeCompare(b, undefined, { numeric: true })).forEach((r, i) => rowOf.set(r, i));
|
|
627
1154
|
}
|
|
628
1155
|
}
|
|
629
1156
|
// cells: sized from body plus margins, positions snapped to the grid
|
|
630
1157
|
const cellDims = new Map();
|
|
631
1158
|
for (const m of members) {
|
|
632
1159
|
const b = bodyBoundsOf(m.sym);
|
|
1160
|
+
const mo = measured?.get(m.key);
|
|
1161
|
+
if (mo) {
|
|
1162
|
+
// the cell is what the previous round drew around this body, a pad
|
|
1163
|
+
// past it on every side: the body sits at shelfL + MARGIN from the
|
|
1164
|
+
// cell's left edge and topPad + MARGIN from its top
|
|
1165
|
+
const shelfL = Math.max(0, ceilU(mo.left) + MEASURE_PAD - MARGIN);
|
|
1166
|
+
const shelfR = Math.max(0, ceilU(mo.right) + MEASURE_PAD - MARGIN);
|
|
1167
|
+
const topPad = Math.max(0, ceilU(mo.top) + MEASURE_PAD - VMARGIN);
|
|
1168
|
+
const below = Math.max(VMARGIN, ceilU(mo.bottom) + MEASURE_PAD);
|
|
1169
|
+
cellDims.set(m.key, {
|
|
1170
|
+
w: shelfL + MARGIN + ceilU(b.maxX - b.minX) + MARGIN + shelfR,
|
|
1171
|
+
h: topPad + VMARGIN + ceilU(b.maxY - b.minY) + below,
|
|
1172
|
+
body: b,
|
|
1173
|
+
shelfL,
|
|
1174
|
+
shelfR,
|
|
1175
|
+
topPad,
|
|
1176
|
+
});
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
// a rail or ground on a pin facing up or down draws a stub, a bar and
|
|
1180
|
+
// a name past the body: that pin's side reserves the symbol's reach
|
|
1181
|
+
const powered = (dy) => m.sym.pins.some((p) => outward(p).dy === dy && netByEndpoint.has(`${m.ref}.${p.number}`) && (netClasses.get(netByEndpoint.get(`${m.ref}.${p.number}`).name)?.cls ?? 'signal') !== 'signal');
|
|
1182
|
+
const topReach = powered(-1) ? POWER_REACH : 0;
|
|
1183
|
+
const botReach = powered(1) ? POWER_REACH : 0;
|
|
633
1184
|
cellDims.set(m.key, {
|
|
634
1185
|
w: ceilU(b.maxX - b.minX) + 2 * MARGIN,
|
|
635
|
-
h: ceilU(b.maxY - b.minY) +
|
|
1186
|
+
h: Math.max(0, topReach - VMARGIN) + VMARGIN + ceilU(b.maxY - b.minY) + VMARGIN + Math.max(0, botReach - VMARGIN),
|
|
636
1187
|
body: b,
|
|
1188
|
+
shelfL: 0,
|
|
1189
|
+
shelfR: 0,
|
|
1190
|
+
topPad: Math.max(0, topReach - VMARGIN),
|
|
637
1191
|
});
|
|
638
1192
|
}
|
|
1193
|
+
const hangs = [];
|
|
1194
|
+
const inlines = [];
|
|
1195
|
+
const hungKeys = new Set();
|
|
1196
|
+
for (const [k, v] of boundTo)
|
|
1197
|
+
if (groupOf.get(k) === gname)
|
|
1198
|
+
boundTo.delete(k), void v;
|
|
1199
|
+
/**
|
|
1200
|
+
* The rotation (0, 90, 180, 270) that turns two-lead part `key` so that
|
|
1201
|
+
* pin `nearPin` faces direction `want` (schematic dx/dy) and its other
|
|
1202
|
+
* lead faces the opposite way; null when the part is not a two-lead
|
|
1203
|
+
* part with opposed leads. This is what lets a diode, LED, fuse or
|
|
1204
|
+
* switch (horizontal leads in the library) hang like a resistor, and a
|
|
1205
|
+
* resistor lie along a row like a diode.
|
|
1206
|
+
*/
|
|
1207
|
+
/** A test point: one pin, probing the net it sits on. It hangs beside that net's pin. */
|
|
1208
|
+
const isTestPoint = (key) => {
|
|
1209
|
+
const inst = instByKey.get(key);
|
|
1210
|
+
return !!inst && inst.sym.pins.length === 1 && (/TestPoint/i.test(inst.part.libId) || /^TP\d/.test(inst.ref));
|
|
1211
|
+
};
|
|
1212
|
+
const orientFor = (key, nearPin, want) => {
|
|
1213
|
+
const pins = instByKey.get(key)?.sym.pins ?? [];
|
|
1214
|
+
if (pins.length !== 2 && !isTestPoint(key))
|
|
1215
|
+
return null;
|
|
1216
|
+
for (const rot of [0, 90, 180, 270]) {
|
|
1217
|
+
const rs = rotatedSym(instByKey.get(key).sym, rot);
|
|
1218
|
+
const near = rs.pins.find((p) => p.number === nearPin);
|
|
1219
|
+
if (!near)
|
|
1220
|
+
return null;
|
|
1221
|
+
const on = outward(near);
|
|
1222
|
+
if (on.dx !== want.dx || on.dy !== want.dy)
|
|
1223
|
+
continue;
|
|
1224
|
+
if (pins.length === 1)
|
|
1225
|
+
return rot;
|
|
1226
|
+
const other = rs.pins.find((p) => p.number !== nearPin);
|
|
1227
|
+
const oo = outward(other);
|
|
1228
|
+
if (oo.dx === -want.dx && oo.dy === -want.dy)
|
|
1229
|
+
return rot;
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
};
|
|
1233
|
+
/** Both leads of a two-lead part, un-rotated; a test point's one pin twice. */
|
|
1234
|
+
const leadsOf = (key) => {
|
|
1235
|
+
const pins = instByKey.get(key)?.sym.pins ?? [];
|
|
1236
|
+
if (isTestPoint(key))
|
|
1237
|
+
return { a: pins[0], b: pins[0] };
|
|
1238
|
+
if (pins.length !== 2)
|
|
1239
|
+
return null;
|
|
1240
|
+
const oa = outward(pins[0]);
|
|
1241
|
+
const ob = outward(pins[1]);
|
|
1242
|
+
if (oa.dx !== -ob.dx || oa.dy !== -ob.dy)
|
|
1243
|
+
return null; // leads must oppose
|
|
1244
|
+
return { a: pins[0], b: pins[1] };
|
|
1245
|
+
};
|
|
1246
|
+
const hangable = (key) => memberKeySet.has(key) &&
|
|
1247
|
+
!decapOwner.has(key) &&
|
|
1248
|
+
!hungKeys.has(key) &&
|
|
1249
|
+
!isConnector(instByKey.get(key).part) &&
|
|
1250
|
+
!/crystal|reson/i.test(instByKey.get(key)?.part.libId ?? '') &&
|
|
1251
|
+
leadsOf(key) !== null;
|
|
1252
|
+
/** Lead span along the hang or run, mm: connection point to connection
|
|
1253
|
+
* point, or for a test point the body's reach past its one pin. */
|
|
1254
|
+
const spanOf = (key, rot = 0) => {
|
|
1255
|
+
const ctl = controlPinOf(key);
|
|
1256
|
+
if (ctl) {
|
|
1257
|
+
const b = bodyBoundsOf(instByKey.get(key).sym);
|
|
1258
|
+
return Math.max(b.maxX - ctl.x, ctl.x - b.minX);
|
|
1259
|
+
}
|
|
1260
|
+
const l = leadsOf(key);
|
|
1261
|
+
if (isTestPoint(key)) {
|
|
1262
|
+
const rs = rotatedSym(instByKey.get(key).sym, rot);
|
|
1263
|
+
const pin = rs.pins[0];
|
|
1264
|
+
const b = bodyBoundsOf(rs);
|
|
1265
|
+
const o = outward(pin);
|
|
1266
|
+
// the body lies opposite the pin's outward direction
|
|
1267
|
+
return o.dy !== 0 ? Math.max(0, o.dy === 1 ? b.maxY - pin.y : pin.y - b.minY) : Math.max(0, o.dx === 1 ? pin.x - b.minX : b.maxX - pin.x);
|
|
1268
|
+
}
|
|
1269
|
+
return Math.hypot(l.a.x - l.b.x, l.a.y - l.b.y);
|
|
1270
|
+
};
|
|
1271
|
+
const INLINE_GAP = 4 * U;
|
|
1272
|
+
/**
|
|
1273
|
+
* Claim a hang from IC pin `pin` starting at `first` (an endpoint of the
|
|
1274
|
+
* pin's net). `dir` 1 hangs down from the row, -1 rises above it; the
|
|
1275
|
+
* chain continues through two-endpoint links to a rail, a ground or a
|
|
1276
|
+
* tapped node. The part is turned so its near lead faces the row.
|
|
1277
|
+
*/
|
|
1278
|
+
const claimHang = (icKey, pin, dx, first, dir) => {
|
|
1279
|
+
const chain = [first.key];
|
|
1280
|
+
const nearPins = [first.pin];
|
|
1281
|
+
hungKeys.add(first.key);
|
|
1282
|
+
boundTo.set(first.key, icKey);
|
|
1283
|
+
let ends = 'open';
|
|
1284
|
+
for (;;) {
|
|
1285
|
+
const cur = chain[chain.length - 1];
|
|
1286
|
+
if (isTestPoint(cur))
|
|
1287
|
+
break;
|
|
1288
|
+
const l = leadsOf(cur);
|
|
1289
|
+
const near = nearPins[nearPins.length - 1];
|
|
1290
|
+
const far = l.a.number === near ? l.b : l.a;
|
|
1291
|
+
const beyond = netByEndpoint.get(`${instByKey.get(cur).ref}.${far.number}`);
|
|
1292
|
+
if (!beyond)
|
|
1293
|
+
break;
|
|
1294
|
+
if ((netClasses.get(beyond.name)?.cls ?? 'signal') !== 'signal') {
|
|
1295
|
+
ends = 'power';
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
if (beyond.pins.length !== 2)
|
|
1299
|
+
break;
|
|
1300
|
+
const next = beyond.pins.map(epKey).find((e) => e !== null && e.key !== cur);
|
|
1301
|
+
if (!next || !hangable(next.key) || chain.includes(next.key))
|
|
1302
|
+
break;
|
|
1303
|
+
chain.push(next.key);
|
|
1304
|
+
nearPins.push(next.pin);
|
|
1305
|
+
hungKeys.add(next.key);
|
|
1306
|
+
boundTo.set(next.key, icKey);
|
|
1307
|
+
}
|
|
1308
|
+
let h = 0;
|
|
1309
|
+
let w = 0;
|
|
1310
|
+
for (const key of chain) {
|
|
1311
|
+
const inst = instByKey.get(key);
|
|
1312
|
+
// turned to hang: the body's extent across the axis and its span along it
|
|
1313
|
+
const rotH = orientFor(key, nearPins[chain.indexOf(key)], { dx: 0, dy: -dir }) ?? 0;
|
|
1314
|
+
const rs = rotatedSym(inst.sym, rotH);
|
|
1315
|
+
const b = bodyBoundsOf(rs);
|
|
1316
|
+
h += spanOf(key, rotH) + HANG_GAP;
|
|
1317
|
+
// a hung part is narrow: its body, the reference and value
|
|
1318
|
+
// beside it, and a unit of air — not the column cell's margins
|
|
1319
|
+
const fieldW = Math.max(inst.ref.length + 1, inst.part.value.length) * TEXT_RESERVE * LABEL_HEIGHT + 1.27;
|
|
1320
|
+
w = Math.max(w, b.maxX - b.minX + fieldW + U);
|
|
1321
|
+
}
|
|
1322
|
+
if (ends === 'power')
|
|
1323
|
+
h += HANG_POWER_END;
|
|
1324
|
+
hangs.push({ ic: icKey, pin, dx, dir, chain, slot: 0, straight: false, w, h, ends });
|
|
1325
|
+
hangNearPins.set(chain.join('+'), nearPins);
|
|
1326
|
+
};
|
|
1327
|
+
const hangNearPins = new Map();
|
|
1328
|
+
const placedOffset = new Set();
|
|
1329
|
+
/**
|
|
1330
|
+
* Claim a series run along the row from IC pin `pin`: `first` lies on
|
|
1331
|
+
* the row with its near lead toward the IC, and the run continues
|
|
1332
|
+
* through two-endpoint links while the next part is a two-lead part.
|
|
1333
|
+
* The far end gets whatever the wire pass gives it: a wire to a
|
|
1334
|
+
* connector on the row, or a label.
|
|
1335
|
+
*/
|
|
1336
|
+
/** Gap before a part on a row: room for the wired net's name above the wire, at least one plain gap. */
|
|
1337
|
+
const gapFor = (netName) => Math.max(INLINE_GAP, grid(ceilU(Math.max(1, netName.length) * TEXT_RESERVE * LABEL_HEIGHT + 2 * U)));
|
|
1338
|
+
const claimInline = (icKey, pin, dx, first) => {
|
|
1339
|
+
const pinNet = netByEndpoint.get(`${instByKey.get(icKey).ref}.${pin.number}`)?.name ?? '';
|
|
1340
|
+
const chain = [{ key: first.key, nearPin: first.pin, gap: gapFor(pinNet) }];
|
|
1341
|
+
hungKeys.add(first.key);
|
|
1342
|
+
boundTo.set(first.key, icKey);
|
|
1343
|
+
let farNet = null;
|
|
1344
|
+
for (let n = 0; n < 3; n++) {
|
|
1345
|
+
const cur = chain[chain.length - 1];
|
|
1346
|
+
if (isTransistor(cur.key)) {
|
|
1347
|
+
farNet = null; // the run ends in the transistor; its other pins take what the wire pass gives
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
const l = leadsOf(cur.key);
|
|
1351
|
+
const far = l.a.number === cur.nearPin ? l.b : l.a;
|
|
1352
|
+
const beyond = netByEndpoint.get(`${instByKey.get(cur.key).ref}.${far.number}`);
|
|
1353
|
+
farNet = beyond?.name ?? null;
|
|
1354
|
+
if (!beyond || beyond.pins.length !== 2)
|
|
1355
|
+
break;
|
|
1356
|
+
const next = beyond.pins.map(epKey).find((e) => e !== null && e.key !== cur.key);
|
|
1357
|
+
if (!next || chain.some((c) => c.key === next.key))
|
|
1358
|
+
break;
|
|
1359
|
+
// a series resistor into a base or gate: the transistor ends the run,
|
|
1360
|
+
// turned so that pin faces the part that drives it
|
|
1361
|
+
if (isTransistor(next.key)) {
|
|
1362
|
+
if (!memberKeySet.has(next.key) || hungKeys.has(next.key) || controlPinOf(next.key).number !== next.pin)
|
|
1363
|
+
break;
|
|
1364
|
+
chain.push({ key: next.key, nearPin: next.pin, gap: gapFor(beyond.name) });
|
|
1365
|
+
hungKeys.add(next.key);
|
|
1366
|
+
boundTo.set(next.key, icKey);
|
|
1367
|
+
farNet = null;
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
if (!hangable(next.key))
|
|
1371
|
+
break;
|
|
1372
|
+
chain.push({ key: next.key, nearPin: next.pin, gap: gapFor(beyond.name) });
|
|
1373
|
+
hungKeys.add(next.key);
|
|
1374
|
+
boundTo.set(next.key, icKey);
|
|
1375
|
+
}
|
|
1376
|
+
let len = 0;
|
|
1377
|
+
for (const c of chain)
|
|
1378
|
+
len += c.gap + spanOf(c.key);
|
|
1379
|
+
// room for the label the far end may carry
|
|
1380
|
+
if (farNet)
|
|
1381
|
+
len += STUB * U + Math.max(1, farNet.length) * TEXT_RESERVE * LABEL_HEIGHT;
|
|
1382
|
+
inlines.push({ ic: icKey, pin, dx, chain, len, farNet, offset: 0 });
|
|
1383
|
+
};
|
|
1384
|
+
const epKey = (ep) => {
|
|
1385
|
+
const m = /^([^.]+)\.(.+)$/.exec(ep);
|
|
1386
|
+
return m ? { key: instKeyOf(m[1], m[2]), pin: m[2] } : null;
|
|
1387
|
+
};
|
|
1388
|
+
// Three units, not four: a hung part's near stub ends one unit past
|
|
1389
|
+
// the gap, and pins sit two units apart, so an even gap parks that
|
|
1390
|
+
// stub end exactly on the neighbouring pin's row, where any run along
|
|
1391
|
+
// that row would join it (every single pull-up beside a pull-down on
|
|
1392
|
+
// the next pin was refused for this). An odd gap keeps it between rows.
|
|
1393
|
+
const HANG_GAP = 3 * U;
|
|
1394
|
+
/**
|
|
1395
|
+
* How far the labels on one side of an anchor actually reach, given
|
|
1396
|
+
* what has been claimed on it: a pin whose parts hang carries no stub
|
|
1397
|
+
* label any more, only its run's name (and only when the net has
|
|
1398
|
+
* endpoints elsewhere), so the lanes may start that much closer to the
|
|
1399
|
+
* IC. `labelExtents` assumes a stub label on every pin and put the
|
|
1400
|
+
* lanes 15 mm further out than the drawing needed.
|
|
1401
|
+
*/
|
|
1402
|
+
const sideReach = (icKey, dx) => {
|
|
1403
|
+
const ic = instByKey.get(icKey);
|
|
1404
|
+
let reach = 0;
|
|
1405
|
+
for (const pin of ic.sym.pins) {
|
|
1406
|
+
if (outward(pin).dx !== dx)
|
|
1407
|
+
continue;
|
|
1408
|
+
const net = netByEndpoint.get(`${ic.ref}.${pin.number}`);
|
|
1409
|
+
if (!net)
|
|
1410
|
+
continue;
|
|
1411
|
+
const nameW = Math.max(1, net.name.length) * TEXT_RESERVE * LABEL_HEIGHT;
|
|
1412
|
+
if ((netClasses.get(net.name)?.cls ?? 'signal') !== 'signal') {
|
|
1413
|
+
reach = Math.max(reach, (STUB + 2) * U + 1.905 + nameW);
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const eps = net.pins.map(epKey);
|
|
1417
|
+
const claimed = eps.some((e) => e !== null && boundTo.get(e.key) === icKey);
|
|
1418
|
+
const elsewhere = eps.some((e) => e === null || (e.key !== icKey && !memberKeySet.has(e.key) && !decapOwner.has(e.key)));
|
|
1419
|
+
if (claimed)
|
|
1420
|
+
reach = Math.max(reach, elsewhere ? nameW + FLAG_PAD + U : STUB * U);
|
|
1421
|
+
else
|
|
1422
|
+
reach = Math.max(reach, STUB * U + nameW + FLAG_PAD);
|
|
1423
|
+
}
|
|
1424
|
+
return reach;
|
|
1425
|
+
};
|
|
1426
|
+
const HANG_POWER_END = 5 * U;
|
|
1427
|
+
/** Rows a power symbol and its name need past a pin: stub, bar, text. */
|
|
1428
|
+
const POWER_CLEAR_ROWS = 6;
|
|
1429
|
+
// Parts hang on ICs first, then on connectors and switches: a fuse
|
|
1430
|
+
// rises from the jack's pin, a button's pull-up, debounce cap and ESD
|
|
1431
|
+
// diode hang on the switch's signal pin. A switch already hung on an
|
|
1432
|
+
// IC pin (a reset button dropping from EN) anchors nothing itself.
|
|
1433
|
+
const claimAnchors = [
|
|
1434
|
+
...anchors,
|
|
1435
|
+
...members.filter((m) => !anchors.includes(m.key) && (isConnector(m.part) || isSwitch(m.key))).map((m) => m.key),
|
|
1436
|
+
];
|
|
1437
|
+
for (const icKey of claimAnchors) {
|
|
1438
|
+
if (hungKeys.has(icKey))
|
|
1439
|
+
continue;
|
|
1440
|
+
const ic = instByKey.get(icKey);
|
|
1441
|
+
const sidePins = ic.sym.pins
|
|
1442
|
+
.filter((p) => outward(p).dx !== 0)
|
|
1443
|
+
.sort((a, b) => b.y - a.y || a.x - b.x); // top row first (symbol y is up)
|
|
1444
|
+
for (const pin of sidePins) {
|
|
1445
|
+
const dx = outward(pin).dx;
|
|
1446
|
+
const net = netByEndpoint.get(`${ic.ref}.${pin.number}`);
|
|
1447
|
+
if (!net || (netClasses.get(net.name)?.cls ?? 'signal') !== 'signal')
|
|
1448
|
+
continue;
|
|
1449
|
+
// the wire pass draws this net only if it stays in the group with
|
|
1450
|
+
// few endpoints; a net that will be labelled anyway gives no hang
|
|
1451
|
+
const eps = net.pins.map(epKey).filter((e) => e !== null);
|
|
1452
|
+
// A net that leaves the group or carries more endpoints than the
|
|
1453
|
+
// wire pass joins still hangs its local parts: the pull-up on a
|
|
1454
|
+
// fault line that also goes to the MCU, the RC on a button the
|
|
1455
|
+
// MCU reads. The wire pass joins the pin and its hung parts as
|
|
1456
|
+
// one local run and labels it once for the rest of the net
|
|
1457
|
+
// (esp32-amp shipped fourteen such parts as labelled islands).
|
|
1458
|
+
if (eps.length > MAX_WIRED_ENDPOINTS)
|
|
1459
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${eps.length} endpoints, local parts still hang`);
|
|
1460
|
+
// a crystal's load caps belong to the flanking idiom below
|
|
1461
|
+
if (eps.some((e) => /crystal|reson/i.test(instByKey.get(e.key)?.part.libId ?? '')))
|
|
1462
|
+
continue;
|
|
1463
|
+
// the chain enters the first part through whichever lead is on the
|
|
1464
|
+
// pin's net: through the top lead it hangs DOWN from the row,
|
|
1465
|
+
// through the bottom lead it rises above it (the pull-up shape)
|
|
1466
|
+
// every hangable endpoint of the pin's net hangs, each as its own
|
|
1467
|
+
// chain: a compensation network is a series RC AND a shunt C on
|
|
1468
|
+
// one pin, and leaving the shunt in a far column keeps the whole
|
|
1469
|
+
// net past wire span, labelled
|
|
1470
|
+
/** The net on the lead of `key` that is NOT `nearPin`. */
|
|
1471
|
+
const farNetOf = (key, nearPin) => {
|
|
1472
|
+
const l = leadsOf(key);
|
|
1473
|
+
const far = l.a.number === nearPin ? l.b : l.a;
|
|
1474
|
+
return netByEndpoint.get(`${instByKey.get(key).ref}.${far.number}`);
|
|
1475
|
+
};
|
|
1476
|
+
/** Bridges from this pin to another pin of the IC, resolved after the pin's other parts. */
|
|
1477
|
+
const pinBridges = [];
|
|
1478
|
+
for (const first of eps) {
|
|
1479
|
+
if (first.key === icKey)
|
|
1480
|
+
continue;
|
|
1481
|
+
// a transistor driven straight from the pin lies on the row, base to the pin
|
|
1482
|
+
if (isTransistor(first.key) && memberKeySet.has(first.key) && !hungKeys.has(first.key) && controlPinOf(first.key).number === first.pin) {
|
|
1483
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(first.key).ref} lies on the row (transistor)`);
|
|
1484
|
+
claimInline(icKey, pin, dx, first);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (!hangable(first.key)) {
|
|
1488
|
+
const why = !memberKeySet.has(first.key) ? 'not a group member' : decapOwner.has(first.key) ? 'a decoupling cap' : hungKeys.has(first.key) ? 'already claimed' : isConnector(instByKey.get(first.key).part) ? 'a connector' : leadsOf(first.key) === null ? 'not a two-lead part' : 'a crystal';
|
|
1489
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(first.key).ref} not hangable (${why})`);
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
// a test point rises above the row of the pin it probes
|
|
1493
|
+
if (isTestPoint(first.key)) {
|
|
1494
|
+
claimHang(icKey, pin, dx, first, -1);
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
// Look to the END of the run this part starts, through two-endpoint
|
|
1498
|
+
// links: a run that ends on a rail or ground is a SHUNT and hangs
|
|
1499
|
+
// from the row — rising to a rail (a pull-up), dropping to ground
|
|
1500
|
+
// (a pull-down, a filter cap, a series RC to ground). A run whose
|
|
1501
|
+
// far net returns to a pin of this IC BRIDGES the two pins and
|
|
1502
|
+
// hangs toward the other pin's row so that net stays within wire
|
|
1503
|
+
// span (a bootstrap cap between BST and OUT). Everything else is
|
|
1504
|
+
// a SERIES element — a 100 Ω in an I2S line, an output inductor —
|
|
1505
|
+
// and lies along the row, away from the IC, the way the reference
|
|
1506
|
+
// sheet draws it.
|
|
1507
|
+
let cur = first;
|
|
1508
|
+
let farNet = farNetOf(cur.key, cur.pin);
|
|
1509
|
+
const seen = new Set([first.key]);
|
|
1510
|
+
for (let n = 0; n < 4 && farNet && (netClasses.get(farNet.name)?.cls ?? 'signal') === 'signal' && farNet.pins.length === 2; n++) {
|
|
1511
|
+
const next = farNet.pins.map(epKey).find((e) => e !== null && e.key !== cur.key);
|
|
1512
|
+
if (!next || !hangable(next.key) || seen.has(next.key))
|
|
1513
|
+
break;
|
|
1514
|
+
seen.add(next.key);
|
|
1515
|
+
cur = next;
|
|
1516
|
+
farNet = farNetOf(cur.key, cur.pin);
|
|
1517
|
+
}
|
|
1518
|
+
const farCls = farNet ? (netClasses.get(farNet.name)?.cls ?? 'signal') : null;
|
|
1519
|
+
// A bridge (the far net returns to another pin of this IC) lies
|
|
1520
|
+
// on the row of the pin that has NOTHING ELSE on it: claimed
|
|
1521
|
+
// from the busier pin it blocked that pin's row, and the series
|
|
1522
|
+
// part behind it could not be wired (esp32-amp's C28 on OUT_LN
|
|
1523
|
+
// left L3 a labelled island while C27 on BST_AP let L2 wire).
|
|
1524
|
+
const bridgeEp = cur.key === first.key && farNet && farCls === 'signal' && farNet.pins.length === 2
|
|
1525
|
+
? farNet.pins.find((ep) => ep.startsWith(`${ic.ref}.`) && ep !== `${ic.ref}.${pin.number}`)
|
|
1526
|
+
: undefined;
|
|
1527
|
+
const otherPin = bridgeEp ? ic.sym.pins.find((p) => `${ic.ref}.${p.number}` === bridgeEp) : undefined;
|
|
1528
|
+
if (otherPin) {
|
|
1529
|
+
const others = eps.filter((e) => e.key !== icKey && e.key !== first.key && (hangable(e.key) || boundTo.get(e.key) === icKey));
|
|
1530
|
+
pinBridges.push({ first, otherPin, othersOnPin: others.length > 0 });
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
// A part between two pins of this IC (a bootstrap cap between
|
|
1534
|
+
// BST and OUT) lies along its row too: hung, its far lead
|
|
1535
|
+
// landed rows away from the other pin and that pin's branch ran
|
|
1536
|
+
// along a third row under a label. On the row, the far net
|
|
1537
|
+
// reaches the other pin's row through the router's trunk.
|
|
1538
|
+
// A bridge to another pin of this IC (a bootstrap cap between BST
|
|
1539
|
+
// and SW) hangs in a lane toward that pin's row, its far lead
|
|
1540
|
+
// joined to that row by one short jog: laid along the row it sat
|
|
1541
|
+
// past every lane and every series part, and the bank under the
|
|
1542
|
+
// group followed the row out (rails 112 mm wide, 91 by hand).
|
|
1543
|
+
if (farCls === 'ground') {
|
|
1544
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(first.key).ref} hangs down (to ${farNet.name})`);
|
|
1545
|
+
claimHang(icKey, pin, dx, first, 1);
|
|
1546
|
+
}
|
|
1547
|
+
else if (farCls === 'rail') {
|
|
1548
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(first.key).ref} rises (to ${farNet.name})`);
|
|
1549
|
+
claimHang(icKey, pin, dx, first, -1);
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(first.key).ref} lies on the row (far net ${farNet?.name ?? 'none'})`);
|
|
1553
|
+
claimInline(icKey, pin, dx, first);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// A bridge cap (BST to SW) hangs in the lane this pin already has,
|
|
1557
|
+
// toward the other pin's row, when the pin has a hang to share the
|
|
1558
|
+
// lane with (the buck's L1 rising from SW): one axis, one T, and the
|
|
1559
|
+
// other row reaches the cap's far lead with a single jog. With no
|
|
1560
|
+
// lane of its own to join it stays on the row (hung alone it cost
|
|
1561
|
+
// four attachments on the amplifier's four bootstrap pairs); and it
|
|
1562
|
+
// is left to the other pin when this pin carries series parts and
|
|
1563
|
+
// that pin carries nothing else.
|
|
1564
|
+
const pinHasHang = hangs.some((hg) => hg.ic === icKey && hg.pin.number === pin.number);
|
|
1565
|
+
for (const b of pinBridges) {
|
|
1566
|
+
if (hungKeys.has(b.first.key))
|
|
1567
|
+
continue;
|
|
1568
|
+
if (pinHasHang) {
|
|
1569
|
+
const dir = b.otherPin.y < pin.y ? 1 : -1; // symbol y is up: a lower pin means dropping
|
|
1570
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(b.first.key).ref} bridges to ${ic.ref}.${b.otherPin.number}; shares the pin's lane, ${dir === 1 ? 'down' : 'up'}`);
|
|
1571
|
+
claimHang(icKey, pin, dx, b.first, dir);
|
|
1572
|
+
}
|
|
1573
|
+
else if (b.othersOnPin) {
|
|
1574
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(b.first.key).ref} bridges to ${ic.ref}.${b.otherPin.number}; left for that pin`);
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
trace(`${ic.ref}.${pin.number} ${net.name}: ${instByKey.get(b.first.key).ref} bridges to ${ic.ref}.${b.otherPin.number}; lies on the row`);
|
|
1578
|
+
claimInline(icKey, pin, dx, b.first);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// Slots per side. A hang whose rows are free of every other pin on
|
|
1583
|
+
// that side stays dead straight on its stub axis (AC-16.31: the wire
|
|
1584
|
+
// from the pin continues into the chain with no bend); the rest take
|
|
1585
|
+
// the first shelf slot whose rows are free. Rows are relative to the
|
|
1586
|
+
// IC origin, y down; symbol pin y is up.
|
|
1587
|
+
for (const dx of [-1, 1]) {
|
|
1588
|
+
const side = hangs.filter((hg) => hg.ic === icKey && hg.dx === dx);
|
|
1589
|
+
const inlineSide = inlines.filter((il) => il.ic === icKey && il.dx === dx);
|
|
1590
|
+
// Two horizontal parts on rows one pitch apart cannot share an x
|
|
1591
|
+
// range: a run on a row next to an earlier run starts past that
|
|
1592
|
+
// run's end (the reference sheet's output filter alternates the
|
|
1593
|
+
// inductor and the cap across its rows for the same reason).
|
|
1594
|
+
// a part lying on a row carries its reference above and its value
|
|
1595
|
+
// below, so its texts reach the neighbouring rows: runs within
|
|
1596
|
+
// three rows of each other stagger (two left C27's value under L3's
|
|
1597
|
+
// reference)
|
|
1598
|
+
const ROWS_NEAR = 3 * 2.54 + 0.01;
|
|
1599
|
+
for (const il of [...inlineSide].sort((a, b) => b.pin.y - a.pin.y)) {
|
|
1600
|
+
let offset = 0;
|
|
1601
|
+
for (;;) {
|
|
1602
|
+
const clash = inlineSide.find((o) => o !== il && o.offset !== undefined && placedOffset.has(o) && Math.abs(o.pin.y - il.pin.y) <= ROWS_NEAR && offset < o.offset + o.len && offset + il.len > o.offset);
|
|
1603
|
+
if (!clash)
|
|
1604
|
+
break;
|
|
1605
|
+
offset = clash.offset + clash.len + INLINE_GAP;
|
|
1606
|
+
}
|
|
1607
|
+
il.offset = offset;
|
|
1608
|
+
placedOffset.add(il);
|
|
1609
|
+
}
|
|
1610
|
+
if (!side.length && !inlineSide.length)
|
|
1611
|
+
continue;
|
|
1612
|
+
// any connected neighbour blocks the straight axis: a signal pin's
|
|
1613
|
+
// stub end sits on it, and a rail or ground pin's longer stub
|
|
1614
|
+
// crosses it — legal, but a wire through the IC's supply stubs is
|
|
1615
|
+
// exactly the crossing a drafter moves a part to avoid (the Tier C
|
|
1616
|
+
// divider drew two of them). A no-connect neighbour blocks nothing.
|
|
1617
|
+
const sidePinYs = ic.sym.pins
|
|
1618
|
+
.filter((p) => outward(p).dx === dx && netByEndpoint.has(`${ic.ref}.${p.number}`))
|
|
1619
|
+
.map((p) => -p.y);
|
|
1620
|
+
const pitch = Math.max(0, ...side.map((hg) => hg.w)) + U;
|
|
1621
|
+
const slots = [];
|
|
1622
|
+
let anyShelved = false;
|
|
1623
|
+
// Slot order matters: a lower pin's branch runs along its row from
|
|
1624
|
+
// the stub to its chain's axis, and a chain hanging down from a
|
|
1625
|
+
// higher pin must not cross that row inside the branch's span. So
|
|
1626
|
+
// down-hangs take slots from the lowest pin upward and up-hangs from
|
|
1627
|
+
// the highest pin downward: the chain that passes other pins' rows
|
|
1628
|
+
// is always the outer one, and every branch stays clear of every
|
|
1629
|
+
// sibling's stub end (esp32-amp's RT branch ran into the COMP
|
|
1630
|
+
// hang's stub end the first time).
|
|
1631
|
+
// Slots go to PINS, not hangs: a part rising and a part dropping
|
|
1632
|
+
// from one pin share one axis — the divider on its pin — and meet
|
|
1633
|
+
// the row in a single T. A slot's occupied range runs from the pin
|
|
1634
|
+
// ROW to the chain end, because the branch along the row belongs
|
|
1635
|
+
// to it: measured from the first part instead, a chain rising from
|
|
1636
|
+
// a lower pin shared its slot with the higher pin's drop and the
|
|
1637
|
+
// two axes met on that pin's row (esp32-amp's USB_OV divider was
|
|
1638
|
+
// refused for exactly that, four hangs on two adjacent pins).
|
|
1639
|
+
// One axis carries at most one chain up and one chain down; a
|
|
1640
|
+
// third chain on the same pin (the NTC divider's cap beside its
|
|
1641
|
+
// thermistor) takes the next lane out, and each lane is slotted as
|
|
1642
|
+
// its own group.
|
|
1643
|
+
const byPin = new Map();
|
|
1644
|
+
for (const hg of side)
|
|
1645
|
+
byPin.set(hg.pin.number, [...(byPin.get(hg.pin.number) ?? []), hg]);
|
|
1646
|
+
const pinRow = (g) => -g[0].pin.y;
|
|
1647
|
+
const pinGroups = [];
|
|
1648
|
+
for (const hgs of byPin.values()) {
|
|
1649
|
+
const ups = hgs.filter((hg) => hg.dir === -1);
|
|
1650
|
+
const downs = hgs.filter((hg) => hg.dir === 1);
|
|
1651
|
+
for (let i = 0; i < Math.max(ups.length, downs.length); i++) {
|
|
1652
|
+
const lane = [ups[i], downs[i]].filter((hg) => hg !== undefined);
|
|
1653
|
+
pinGroups.push(lane);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const ordered = [
|
|
1657
|
+
...pinGroups.filter((g) => g.some((hg) => hg.dir === 1)).sort((a, b) => pinRow(b) - pinRow(a)),
|
|
1658
|
+
...pinGroups.filter((g) => g.every((hg) => hg.dir === -1)).sort((a, b) => pinRow(a) - pinRow(b)),
|
|
1659
|
+
];
|
|
1660
|
+
const lowRowAll = Math.max(...side.map((hg) => -hg.pin.y));
|
|
1661
|
+
const highRowAll = Math.min(...side.map((hg) => -hg.pin.y));
|
|
1662
|
+
for (const g of ordered) {
|
|
1663
|
+
const rowY = pinRow(g);
|
|
1664
|
+
const top = Math.min(rowY, ...g.map((hg) => (hg.dir === -1 ? highRowAll - HANG_GAP - hg.h : rowY)));
|
|
1665
|
+
const bot = Math.max(rowY, ...g.map((hg) => (hg.dir === 1 ? lowRowAll + HANG_GAP + hg.h : rowY)));
|
|
1666
|
+
// the clearance check pads the axis range by four units past the
|
|
1667
|
+
// pin row and the chain end, so a neighbour that close blocks it
|
|
1668
|
+
// the same five units (four of pad plus one) the clearance check
|
|
1669
|
+
// itself applies, else a neighbour exactly at the edge passes here
|
|
1670
|
+
// and refuses there (J1's fuse against the CC1 pin two rows down)
|
|
1671
|
+
const straight = !sidePinYs.some((y) => y !== rowY && y > top - 5 * U && y < bot + 5 * U);
|
|
1672
|
+
for (const hg of g)
|
|
1673
|
+
hg.straight = straight;
|
|
1674
|
+
if (straight)
|
|
1675
|
+
continue;
|
|
1676
|
+
anyShelved = true;
|
|
1677
|
+
let k = 0;
|
|
1678
|
+
while ((slots[k] ?? []).some((iv) => top < iv.bot + 2 * U && bot > iv.top - 2 * U))
|
|
1679
|
+
k++;
|
|
1680
|
+
(slots[k] ??= []).push({ top, bot });
|
|
1681
|
+
for (const hg of g)
|
|
1682
|
+
hg.slot = k;
|
|
1683
|
+
}
|
|
1684
|
+
const dims = cellDims.get(icKey);
|
|
1685
|
+
const ext = labelExtents([icKey]);
|
|
1686
|
+
// Lanes come first, past the side's labels; the inline runs start
|
|
1687
|
+
// beyond the lanes. A part lying on the row between the stub and
|
|
1688
|
+
// the lanes blocked the row for the pin's own hung parts (Q1 on
|
|
1689
|
+
// VBUS_FET_EN left R17 unreachable), and a run on another row
|
|
1690
|
+
// starting inside the lanes crossed their bodies.
|
|
1691
|
+
const laneReach = sideReach(icKey, dx);
|
|
1692
|
+
void ext;
|
|
1693
|
+
const lanesW = anyShelved ? slots.length * pitch : 0;
|
|
1694
|
+
for (const il of inlineSide)
|
|
1695
|
+
il.offset += anyShelved ? ceilU(laneReach + 2 * U) * U + lanesW : 0;
|
|
1696
|
+
const inlineW = Math.max(0, ...inlineSide.map((il) => il.offset + il.len));
|
|
1697
|
+
// shelf: the IC's own labels, the stub, the lanes, then the runs. A
|
|
1698
|
+
// straight hang sits within the stub and the cell's own margin and
|
|
1699
|
+
// channel, so a side whose hangs are all straight and carries no
|
|
1700
|
+
// inline run reserves nothing (a five-part board went from A5 to A4
|
|
1701
|
+
// when it did).
|
|
1702
|
+
const shelf = anyShelved ? ceilU(laneReach + STUB * U) + 2 + Math.ceil(lanesW / U) + (inlineSide.length ? ceilU(inlineW - lanesW - laneReach) : 0) : inlineSide.length ? ceilU(inlineW + STUB * U) + 2 : 0;
|
|
1703
|
+
if (!measured?.has(icKey)) {
|
|
1704
|
+
if (dx === 1)
|
|
1705
|
+
dims.shelfR = shelf;
|
|
1706
|
+
else
|
|
1707
|
+
dims.shelfL = shelf;
|
|
1708
|
+
dims.w += shelf;
|
|
1709
|
+
}
|
|
1710
|
+
// y-down relative to the IC origin: the body spans -maxY..-minY
|
|
1711
|
+
const lowRow = Math.max(...side.map((hg) => -hg.pin.y));
|
|
1712
|
+
const highRow = Math.min(...side.map((hg) => -hg.pin.y));
|
|
1713
|
+
const deepest = Math.max(-dims.body.minY, ...side.map((hg) => (hg.dir === 1 ? (hg.straight ? -hg.pin.y : lowRow) + HANG_GAP + hg.h : -hg.pin.y)), ...inlineSide.map((il) => -il.pin.y + 3 * U));
|
|
1714
|
+
// what this side uses below the body's top, and how far its labels
|
|
1715
|
+
// reach: the shelf below that is free for the infill pass
|
|
1716
|
+
const laneDepth = Math.max(-dims.body.maxY, ...side.map((hg) => (hg.dir === 1 ? (hg.straight ? -hg.pin.y : lowRow) + HANG_GAP + hg.h : -hg.pin.y)), ...inlineSide.map((il) => -il.pin.y + 3 * U));
|
|
1717
|
+
const usedBelow = ceilU(laneDepth - -dims.body.maxY) + 2;
|
|
1718
|
+
if (dx === 1) {
|
|
1719
|
+
dims.usedR = usedBelow;
|
|
1720
|
+
dims.reachR = ceilU(laneReach + 2 * U);
|
|
1721
|
+
}
|
|
1722
|
+
else {
|
|
1723
|
+
dims.usedL = usedBelow;
|
|
1724
|
+
dims.reachL = ceilU(laneReach + 2 * U);
|
|
1725
|
+
}
|
|
1726
|
+
const highest = Math.min(-dims.body.maxY, ...side.map((hg) => (hg.dir === -1 ? (hg.straight ? -hg.pin.y : highRow) - HANG_GAP - hg.h : -hg.pin.y)));
|
|
1727
|
+
// an inline run lies on its row; a transistor at its end reaches two
|
|
1728
|
+
// rows above and below it (collector and emitter stubs)
|
|
1729
|
+
for (const il of inlineSide) {
|
|
1730
|
+
for (const c of il.chain) {
|
|
1731
|
+
if (!controlPinOf(c.key))
|
|
1732
|
+
continue;
|
|
1733
|
+
const b = bodyBoundsOf(instByKey.get(c.key).sym);
|
|
1734
|
+
const reach = Math.max(b.maxY, -b.minY) + STUB * U + POWER_CLEAR_ROWS * U;
|
|
1735
|
+
if (measured?.has(icKey))
|
|
1736
|
+
continue;
|
|
1737
|
+
dims.topPad = Math.max(dims.topPad, ceilU(-dims.body.maxY - (-il.pin.y - reach)));
|
|
1738
|
+
dims.h = Math.max(dims.h, ceilU(Math.max(deepest, -il.pin.y + reach) - Math.min(highest, -il.pin.y - reach)) + VMARGIN + 2);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (!measured?.has(icKey)) {
|
|
1742
|
+
dims.topPad = Math.max(dims.topPad, ceilU(-dims.body.maxY - highest));
|
|
1743
|
+
// the chain's end already carries the symbol's own room; two
|
|
1744
|
+
// units of air below it, not a full margin (a button block's cell
|
|
1745
|
+
// was 39 mm tall where a drafter draws it on a 25 mm pitch)
|
|
1746
|
+
dims.h = Math.max(dims.h, ceilU(deepest - highest) + VMARGIN + 2);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
// Node hangs: the far end of a series run is a node too. An RC filter's
|
|
1751
|
+
// cap, a button's debounce cap and ESD diode sit on the net PAST the
|
|
1752
|
+
// series resistor, so they hang from the run's last part, dropping or
|
|
1753
|
+
// rising from the row just beyond its far lead, the first straight down
|
|
1754
|
+
// from the row's end and the rest on lanes past it.
|
|
1755
|
+
const nodeAnchors = new Set();
|
|
1756
|
+
for (const il of [...inlines]) {
|
|
1757
|
+
const last = il.chain[il.chain.length - 1];
|
|
1758
|
+
if (isTransistor(last.key))
|
|
1759
|
+
continue;
|
|
1760
|
+
const l = leadsOf(last.key);
|
|
1761
|
+
const farPin = l.a.number === last.nearPin ? l.b : l.a;
|
|
1762
|
+
const farNet = netByEndpoint.get(`${instByKey.get(last.key).ref}.${farPin.number}`);
|
|
1763
|
+
if (!farNet || (netClasses.get(farNet.name)?.cls ?? 'signal') !== 'signal')
|
|
1764
|
+
continue;
|
|
1765
|
+
const eps = farNet.pins.map(epKey).filter((e) => e !== null);
|
|
1766
|
+
let lanes = 0;
|
|
1767
|
+
for (const first of eps) {
|
|
1768
|
+
if (first.key === last.key || !hangable(first.key))
|
|
1769
|
+
continue;
|
|
1770
|
+
const fl = leadsOf(first.key);
|
|
1771
|
+
const far = fl.a.number === first.pin ? fl.b : fl.a;
|
|
1772
|
+
const beyond = netByEndpoint.get(`${instByKey.get(first.key).ref}.${far.number}`);
|
|
1773
|
+
const cls = beyond ? (netClasses.get(beyond.name)?.cls ?? 'signal') : null;
|
|
1774
|
+
if (cls !== 'ground' && cls !== 'rail')
|
|
1775
|
+
continue; // only shunts; a second series part is a network
|
|
1776
|
+
trace(`${instByKey.get(last.key).ref}.${farPin.number} ${farNet.name} (node): ${instByKey.get(first.key).ref} ${cls === 'ground' ? 'hangs down' : 'rises'}`);
|
|
1777
|
+
claimHang(last.key, farPin, il.dx, first, cls === 'ground' ? 1 : -1);
|
|
1778
|
+
const hg = hangs[hangs.length - 1];
|
|
1779
|
+
hg.slot = lanes;
|
|
1780
|
+
hg.straight = lanes === 0;
|
|
1781
|
+
lanes++;
|
|
1782
|
+
nodeAnchors.add(last.key);
|
|
1783
|
+
}
|
|
1784
|
+
if (!lanes)
|
|
1785
|
+
continue;
|
|
1786
|
+
// the run's owner reserves the lanes and the drop below them
|
|
1787
|
+
const nodeHangs = hangs.filter((hg) => hg.ic === last.key);
|
|
1788
|
+
const pitch = Math.max(0, ...nodeHangs.map((hg) => hg.w)) + U;
|
|
1789
|
+
const add = (lanes - 1) * pitch + 2 * U + Math.max(0, ...nodeHangs.map((hg) => hg.w)) / 2;
|
|
1790
|
+
il.len += add;
|
|
1791
|
+
const dims = cellDims.get(il.ic);
|
|
1792
|
+
if (measured?.has(il.ic))
|
|
1793
|
+
continue;
|
|
1794
|
+
if (il.dx === 1)
|
|
1795
|
+
dims.shelfR += ceilU(add);
|
|
1796
|
+
else
|
|
1797
|
+
dims.shelfL += ceilU(add);
|
|
1798
|
+
dims.w += ceilU(add);
|
|
1799
|
+
const rowRel = -il.pin.y;
|
|
1800
|
+
const hDown = Math.max(0, ...nodeHangs.filter((hg) => hg.dir === 1).map((hg) => hg.h));
|
|
1801
|
+
const hUp = Math.max(0, ...nodeHangs.filter((hg) => hg.dir === -1).map((hg) => hg.h));
|
|
1802
|
+
const bottom = Math.max(-dims.body.minY, rowRel + HANG_GAP + hDown);
|
|
1803
|
+
const top = Math.min(-dims.body.maxY, rowRel - HANG_GAP - hUp);
|
|
1804
|
+
dims.topPad = Math.max(dims.topPad, ceilU(-dims.body.maxY - top));
|
|
1805
|
+
dims.h = Math.max(dims.h, ceilU(bottom - top) + VMARGIN + 2);
|
|
1806
|
+
}
|
|
1807
|
+
void nodeAnchors;
|
|
1808
|
+
for (const col of columns) {
|
|
1809
|
+
for (let i = col.length - 1; i >= 0; i--)
|
|
1810
|
+
if (hungKeys.has(col[i]))
|
|
1811
|
+
col.splice(i, 1);
|
|
1812
|
+
}
|
|
1813
|
+
for (let i = columns.length - 1; i >= 0; i--)
|
|
1814
|
+
if (!columns[i].length)
|
|
1815
|
+
columns.splice(i, 1);
|
|
1816
|
+
// The budget must leave room for the label TEXT facing the sheet edges:
|
|
1817
|
+
// a band filled to the full usable width puts the leftmost column's
|
|
1818
|
+
// left-facing labels outside the frame (#220 phase 1), and no later
|
|
1819
|
+
// shift can fix both edges at once.
|
|
1820
|
+
const ext = groupExtents.get(gname);
|
|
1821
|
+
const bandW = Math.max(1, bandBudgetW - ceilU(ext.left) - ceilU(ext.right));
|
|
1822
|
+
// Height the decoupling bank adds UNDER the columns once they span
|
|
1823
|
+
// `blockW` units: the same row-wrap the bank placement below performs.
|
|
1824
|
+
const bankHeight = (blockW) => {
|
|
1825
|
+
if (!caps.length)
|
|
1826
|
+
return 0;
|
|
1827
|
+
const capBudget = Math.min(bandW, Math.max(64, blockW));
|
|
1828
|
+
let rows = 1;
|
|
1829
|
+
let x = 0;
|
|
1830
|
+
for (const c of caps) {
|
|
1831
|
+
const b = bodyBoundsOf(c.sym);
|
|
1832
|
+
const w = ceilU(b.maxX - b.minX) + 2 * MARGIN;
|
|
1833
|
+
if (x > 0 && x + w > capBudget) {
|
|
1834
|
+
rows++;
|
|
1835
|
+
x = 0;
|
|
1836
|
+
}
|
|
1837
|
+
x += w;
|
|
1838
|
+
}
|
|
1839
|
+
return MARGIN + 4 + rows * (2 * MARGIN + 6);
|
|
1840
|
+
};
|
|
639
1841
|
// Column height budget (#220 phase 4): a depth whose parts stack taller
|
|
640
1842
|
// than the budget splits into several side-by-side columns, in row
|
|
641
1843
|
// order, so a 24-part board stops drafting as one full-height strip on
|
|
642
1844
|
// a sheet two sizes too large. Cells never shrink; only the arrangement
|
|
643
1845
|
// changes, so readability is untouched.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1846
|
+
//
|
|
1847
|
+
// The bank sits under the columns and counts against the SAME budget:
|
|
1848
|
+
// a group whose columns already fit the budget got no shorter under the
|
|
1849
|
+
// compaction retries while its bulk-cap rows pushed the group past the
|
|
1850
|
+
// sheet (a 30-part power-input group missed A1 by 8 mm that way). The
|
|
1851
|
+
// bank's rows are estimated at the columns' pre-split width — a lower
|
|
1852
|
+
// bound on the width they end up with, so an upper bound on rows.
|
|
1853
|
+
const colsW = columns.reduce((s, col) => s + Math.max(...col.map((r) => cellDims.get(r).w)), 0) +
|
|
1854
|
+
CHANNEL * Math.max(0, columns.length - 1);
|
|
1855
|
+
const sheetBudget = colBudgetH === Infinity ? Infinity : Math.max(1, colBudgetH - bankHeight(colsW));
|
|
1856
|
+
// ---------- in-group column balance ----------
|
|
1857
|
+
// A depth whose parts stack into one strip draws the group as a tall
|
|
1858
|
+
// thin column beside a short IC with the rest of the box empty: an
|
|
1859
|
+
// amplifier group put 20 filter caps in one 500 mm column next to a
|
|
1860
|
+
// 120 mm IC, and the box was two-thirds air. A drafter runs such parts
|
|
1861
|
+
// in several columns no taller than the IC they serve. So every column
|
|
1862
|
+
// is budgeted at the taller of the group's tallest IC cell and the side
|
|
1863
|
+
// of the square the group's cells would fill, and one taller than that
|
|
1864
|
+
// splits into equal-height side-by-side columns. The IC's CELL, not its
|
|
1865
|
+
// column: a transistor or an eFuse sharing a column with eight
|
|
1866
|
+
// resistors must not make that strip the reference height. Row order
|
|
1867
|
+
// is preserved chunk by chunk, exactly as the sheet budget splits, so
|
|
1868
|
+
// barycenter ordering and the trunk idioms are untouched.
|
|
1869
|
+
const colH = (col) => col.reduce((s, r, i) => s + cellDims.get(r).h + (i ? ROW_GAP : 0), 0);
|
|
1870
|
+
const isIC = (key) => instByKey.get(key).sym.pins.length >= 3;
|
|
1871
|
+
const anchorH = Math.max(0, ...members.filter((m) => isIC(m.key)).map((m) => cellDims.get(m.key).h));
|
|
1872
|
+
const cellArea = columns.reduce((s, c) => s + colH(c) * Math.max(...c.map((r) => cellDims.get(r).w)), 0);
|
|
1873
|
+
const squareSide = Math.ceil(Math.sqrt(cellArea));
|
|
1874
|
+
// Under a band budget the group is shaped to FILL the band's width, not
|
|
1875
|
+
// to be square: a column of eight test points re-rows into two of four
|
|
1876
|
+
// and the button blocks stack three deep, the grid a person draws when
|
|
1877
|
+
// the group must sit in a 250 mm column (esp32-amp's UI group went
|
|
1878
|
+
// from 364 × 150 to the width of its neighbours).
|
|
1879
|
+
// A group with no IC to lead it (buttons, test points, LEDs) has no
|
|
1880
|
+
// natural shape, and its columns line up as a strip five blocks wide;
|
|
1881
|
+
// shape it no wider than two and a half times its square side, the grid
|
|
1882
|
+
// a person draws. Groups led by an IC keep the IC's own proportions.
|
|
1883
|
+
const shapeToAspect = anchors.length === 0;
|
|
1884
|
+
const aspectBand = Math.ceil(2.5 * squareSide);
|
|
1885
|
+
const searchBand = shapeToAspect ? Math.min(aspectBand, bandW) : bandW;
|
|
1886
|
+
const targetH = shapeToAspect
|
|
1887
|
+
? Math.max(Math.ceil(cellArea / searchBand), Math.ceil(squareSide / 2))
|
|
1888
|
+
: bandBudgetW === Infinity
|
|
1889
|
+
? squareSide
|
|
1890
|
+
: Math.max(Math.ceil(cellArea / bandBudgetW), Math.ceil(squareSide / 2));
|
|
1891
|
+
const balanceH = Math.max(anchorH, targetH);
|
|
1892
|
+
/** Split `col` into the fewest equal-height chunks that each fit
|
|
1893
|
+
* `budget`; a single cell never splits, and a column shorter than
|
|
1894
|
+
* `minCells` is left alone (a drafter does not re-row three parts). */
|
|
1895
|
+
const splitColumn = (col, budget, minCells = 1) => {
|
|
1896
|
+
const total = colH(col);
|
|
1897
|
+
if (total <= budget || col.length < minCells)
|
|
1898
|
+
return [col];
|
|
1899
|
+
const k = Math.ceil(total / budget);
|
|
1900
|
+
const fill = (cap) => {
|
|
647
1901
|
const chunks = [];
|
|
648
1902
|
let cur = [];
|
|
649
1903
|
let h = 0;
|
|
650
1904
|
for (const ref of col) {
|
|
651
1905
|
const add = cellDims.get(ref).h + (cur.length ? ROW_GAP : 0);
|
|
652
|
-
if (cur.length && h + add >
|
|
1906
|
+
if (cur.length && h + add > cap) {
|
|
653
1907
|
chunks.push(cur);
|
|
654
1908
|
cur = [ref];
|
|
655
1909
|
h = cellDims.get(ref).h;
|
|
@@ -662,20 +1916,134 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
662
1916
|
if (cur.length)
|
|
663
1917
|
chunks.push(cur);
|
|
664
1918
|
return chunks;
|
|
665
|
-
}
|
|
1919
|
+
};
|
|
1920
|
+
// Balanced chunks first, for the look of level columns: the split of
|
|
1921
|
+
// the column into k consecutive chunks that minimises the tallest
|
|
1922
|
+
// chunk (a greedy fill to an equal target fell 7-1 on eight test
|
|
1923
|
+
// points of two heights, and 2-2-1 on five equal cells). If even
|
|
1924
|
+
// that tallest chunk is over the budget, fill to the budget instead.
|
|
1925
|
+
const hs = col.map((ref) => cellDims.get(ref).h);
|
|
1926
|
+
const n = hs.length;
|
|
1927
|
+
const runH = (i, j) => hs.slice(i, j + 1).reduce((a, b) => a + b, 0) + ROW_GAP * (j - i);
|
|
1928
|
+
const bestMax = Array.from({ length: k + 1 }, () => new Array(n + 1).fill(Infinity));
|
|
1929
|
+
const cutAt = Array.from({ length: k + 1 }, () => new Array(n + 1).fill(0));
|
|
1930
|
+
bestMax[0][0] = 0;
|
|
1931
|
+
for (let parts = 1; parts <= k; parts++) {
|
|
1932
|
+
for (let j = 1; j <= n; j++) {
|
|
1933
|
+
for (let i = parts - 1; i < j; i++) {
|
|
1934
|
+
const v = Math.max(bestMax[parts - 1][i], runH(i, j - 1));
|
|
1935
|
+
if (v < bestMax[parts][j]) {
|
|
1936
|
+
bestMax[parts][j] = v;
|
|
1937
|
+
cutAt[parts][j] = i;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (bestMax[k][n] > budget)
|
|
1943
|
+
return fill(budget);
|
|
1944
|
+
const chunks = [];
|
|
1945
|
+
for (let parts = k, j = n; parts >= 1; parts--) {
|
|
1946
|
+
const i = cutAt[parts][j];
|
|
1947
|
+
chunks.unshift(col.slice(i, j));
|
|
1948
|
+
j = i;
|
|
1949
|
+
}
|
|
1950
|
+
return chunks;
|
|
1951
|
+
};
|
|
1952
|
+
const BALANCE_MIN_CELLS = 4;
|
|
1953
|
+
// Under a band budget, the lowest column height at which the group's
|
|
1954
|
+
// columns fit the band side by side in ONE band wins: shorter columns
|
|
1955
|
+
// mean more of them and a group that wraps onto bands anyway, taller
|
|
1956
|
+
// ones a group that fills the band's width in one pass (five button
|
|
1957
|
+
// blocks as two columns of three beside two columns of test points).
|
|
1958
|
+
let chosenH = balanceH;
|
|
1959
|
+
if (bandBudgetW !== Infinity || shapeToAspect) {
|
|
1960
|
+
const widthAt = (h) => {
|
|
1961
|
+
const cols = columns.flatMap((col) => splitColumn(col, h, BALANCE_MIN_CELLS));
|
|
1962
|
+
// the same channel and facing-label widening the placement below applies
|
|
1963
|
+
return cols.reduce((sum, c, i) => {
|
|
1964
|
+
const next = cols[i + 1];
|
|
1965
|
+
return sum + Math.max(...c.map((r) => cellDims.get(r).w)) + (next ? CHANNEL + widenBy(labelExtents(c).right, labelExtents(next).left, 2 * MARGIN + CHANNEL) : 0);
|
|
1966
|
+
}, 0);
|
|
1967
|
+
};
|
|
1968
|
+
// against the band the columns actually get (the budget less the
|
|
1969
|
+
// group's own label reserves), not the raw budget
|
|
1970
|
+
const ceiling = Math.max(balanceH, ...columns.map(colH));
|
|
1971
|
+
for (let h = balanceH; h <= ceiling; h += 2) {
|
|
1972
|
+
if (widthAt(h) <= searchBand) {
|
|
1973
|
+
chosenH = h;
|
|
1974
|
+
break;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (widthAt(chosenH) > searchBand)
|
|
1978
|
+
chosenH = shapeToAspect ? balanceH : ceiling;
|
|
1979
|
+
}
|
|
1980
|
+
let columnsToPlace = columns.flatMap((col) => splitColumn(col, chosenH, BALANCE_MIN_CELLS).flatMap((c) => (sheetBudget === Infinity ? [c] : splitColumn(c, sheetBudget))));
|
|
1981
|
+
// Shelf infill. An IC's side shelf is as deep as its tallest lane, but
|
|
1982
|
+
// the lanes hang from the top pins and the shelf below them is empty;
|
|
1983
|
+
// small cells from later columns move into that room (the USB ESD
|
|
1984
|
+
// array under the UART bridge's transistor runs, where the hand-laid
|
|
1985
|
+
// sheet keeps it), and the column they leave narrows or disappears.
|
|
1986
|
+
const infill = new Map();
|
|
1987
|
+
for (let ci = 0; ci < columnsToPlace.length; ci++) {
|
|
1988
|
+
for (const host of columnsToPlace[ci]) {
|
|
1989
|
+
const d = cellDims.get(host);
|
|
1990
|
+
for (const side of [1, -1]) {
|
|
1991
|
+
const shelf = side === 1 ? d.shelfR : d.shelfL;
|
|
1992
|
+
const used = side === 1 ? d.usedR : d.usedL;
|
|
1993
|
+
const reach = side === 1 ? d.reachR ?? 0 : d.reachL ?? 0;
|
|
1994
|
+
if (!shelf || used === undefined)
|
|
1995
|
+
continue;
|
|
1996
|
+
const freeW = shelf - reach - 1;
|
|
1997
|
+
let top = d.topPad + VMARGIN + used;
|
|
1998
|
+
let freeH = d.h - top - 1;
|
|
1999
|
+
trace(`infill room: ${instByKey.get(host).ref} ${side === 1 ? 'right' : 'left'} shelf ${shelf}u reach ${reach}u used ${used}u -> free ${freeW}x${freeH}u`);
|
|
2000
|
+
if (freeW < 8 || freeH < 8)
|
|
2001
|
+
continue;
|
|
2002
|
+
// candidates from every other column, later ones first; a cell
|
|
2003
|
+
// leaving the host's own column shortens it
|
|
2004
|
+
const order = [...columnsToPlace.keys()].sort((a, b) => (a > ci ? 0 : a === ci ? 1 : 2) - (b > ci ? 0 : b === ci ? 1 : 2) || a - b);
|
|
2005
|
+
for (const cj of order) {
|
|
2006
|
+
if (freeH < 8)
|
|
2007
|
+
break;
|
|
2008
|
+
const col = columnsToPlace[cj];
|
|
2009
|
+
for (let k = col.length - 1; k >= 0 && freeH >= 8; k--) {
|
|
2010
|
+
const cand = col[k];
|
|
2011
|
+
if (cand === host)
|
|
2012
|
+
continue;
|
|
2013
|
+
// connectors keep the left column (the reader looks for them
|
|
2014
|
+
// there); anchors and anything with lanes of its own stay put
|
|
2015
|
+
// only a substantial part (four pins or more: an ESD array, a
|
|
2016
|
+
// level shifter) is worth tucking away; scattering test points
|
|
2017
|
+
// and LEDs into shelves reads as clutter, not economy
|
|
2018
|
+
const cinst = instByKey.get(cand);
|
|
2019
|
+
if (isConnector(cinst.part) || cinst.sym.pins.length < 4)
|
|
2020
|
+
continue;
|
|
2021
|
+
const cd = cellDims.get(cand);
|
|
2022
|
+
if (cd.shelfL || cd.shelfR || cd.w > freeW || cd.h > freeH)
|
|
2023
|
+
continue;
|
|
2024
|
+
infill.set(host, [...(infill.get(host) ?? []), { key: cand, side, relY: top }]);
|
|
2025
|
+
col.splice(k, 1);
|
|
2026
|
+
trace(`infill: ${instByKey.get(cand).ref} moves under the ${side === 1 ? 'right' : 'left'} shelf of ${instByKey.get(host).ref}`);
|
|
2027
|
+
top += cd.h + ROW_GAP;
|
|
2028
|
+
freeH -= cd.h + ROW_GAP;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
columnsToPlace = columnsToPlace.filter((c) => c.length);
|
|
2035
|
+
const hostGeom = new Map();
|
|
2036
|
+
trace(`group "${gname}" columns (band ${bandBudgetW === Infinity ? 'inf' : bandBudgetW}u, col budget ${colBudgetH === Infinity ? 'inf' : colBudgetH}u, balance ${balanceH}u chosen ${chosenH}u): ${columnsToPlace.map((c) => `[${c.map((k) => `${instByKey.get(k).ref}:${cellDims.get(k).w}x${cellDims.get(k).h}`).join(' ')}]`).join(' ')}`);
|
|
666
2037
|
let colX = groupX;
|
|
667
2038
|
let groupMaxY = 0;
|
|
668
2039
|
let bandTop = 0; // y origin (units) of the current band of columns
|
|
669
2040
|
let bandCount = 1;
|
|
670
|
-
// The budget must leave room for the label TEXT facing the sheet edges:
|
|
671
|
-
// a band filled to the full usable width puts the leftmost column's
|
|
672
|
-
// left-facing labels outside the frame (#220 phase 1), and no later
|
|
673
|
-
// shift can fix both edges at once.
|
|
674
|
-
const ext = groupExtents.get(gname);
|
|
675
|
-
const bandW = Math.max(1, bandBudgetW - ceilU(ext.left) - ceilU(ext.right));
|
|
676
2041
|
for (let ci = 0; ci < columnsToPlace.length; ci++) {
|
|
677
2042
|
const col = columnsToPlace[ci];
|
|
678
2043
|
const colW = Math.max(...col.map((r) => cellDims.get(r).w));
|
|
2044
|
+
const colShelfL = Math.max(...col.map((r) => cellDims.get(r).shelfL));
|
|
2045
|
+
const colShelfR = Math.max(...col.map((r) => cellDims.get(r).shelfR));
|
|
2046
|
+
const colCoreW = Math.max(...col.map((r) => cellDims.get(r).w - cellDims.get(r).shelfL - cellDims.get(r).shelfR));
|
|
679
2047
|
// Banding (#219): a column that would tile past the width budget starts
|
|
680
2048
|
// a new band of columns below everything placed so far, the way the
|
|
681
2049
|
// shelf-wrap below re-rows whole groups. Never before the first column
|
|
@@ -689,9 +2057,14 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
689
2057
|
let rowY = bandTop;
|
|
690
2058
|
for (const ref of col) {
|
|
691
2059
|
const dims = cellDims.get(ref);
|
|
692
|
-
|
|
693
|
-
|
|
2060
|
+
// shared column axis (units): the core span between the shelves a
|
|
2061
|
+
// hung IC reserves beside itself
|
|
2062
|
+
const cx = colX + colShelfL + Math.floor((colW - colShelfL - colShelfR - Math.max(0, colW - colShelfL - colShelfR - colCoreW)) / 2);
|
|
694
2063
|
const b = dims.body;
|
|
2064
|
+
// the body sits at the top of its cell, under the room any up-hang
|
|
2065
|
+
// needs; a cell with nothing hung is body plus margins, so this is
|
|
2066
|
+
// its centre as before
|
|
2067
|
+
const cy = rowY + dims.topPad + VMARGIN + Math.floor(ceilU(b.maxY - b.minY) / 2);
|
|
695
2068
|
// origin so the body centers on the cell center, snapped to grid
|
|
696
2069
|
const ox = grid(cx - Math.round((b.minX + b.maxX) / 2 / U));
|
|
697
2070
|
const oy = grid(cy + Math.round((b.minY + b.maxY) / 2 / U));
|
|
@@ -706,7 +2079,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
706
2079
|
body: { minX: ox + b.minX, minY: oy - b.maxY, maxX: ox + b.maxX, maxY: oy - b.minY },
|
|
707
2080
|
cellW: dims.w,
|
|
708
2081
|
cellH: dims.h,
|
|
2082
|
+
rot: 0,
|
|
709
2083
|
});
|
|
2084
|
+
if (infill.has(ref))
|
|
2085
|
+
hostGeom.set(ref, { colX, colW, colShelfR, rowY });
|
|
710
2086
|
rowY += dims.h + ROW_GAP;
|
|
711
2087
|
}
|
|
712
2088
|
groupMaxY = Math.max(groupMaxY, rowY - ROW_GAP);
|
|
@@ -714,7 +2090,20 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
714
2090
|
colX += colW + CHANNEL + (next ? widenBy(labelExtents(col).right, labelExtents(next).left, 2 * MARGIN + CHANNEL) : 0);
|
|
715
2091
|
}
|
|
716
2092
|
// decoupling rows: caps in a uniform row under their owner (or the group)
|
|
717
|
-
|
|
2093
|
+
// by rail first, then by reference: caps of one rail sit side by side
|
|
2094
|
+
// and share one trunk and one pair of symbols (sorted by reference
|
|
2095
|
+
// alone, a +5V cap numbered after the +3V3 caps stood apart with its
|
|
2096
|
+
// own two symbols)
|
|
2097
|
+
const railOf = (key) => {
|
|
2098
|
+
const inst = instByKey.get(key);
|
|
2099
|
+
for (const p of inst.sym.pins) {
|
|
2100
|
+
const n = netByEndpoint.get(`${inst.ref}.${p.number}`);
|
|
2101
|
+
if (n && (netClasses.get(n.name)?.cls ?? 'signal') === 'rail')
|
|
2102
|
+
return n.name;
|
|
2103
|
+
}
|
|
2104
|
+
return '';
|
|
2105
|
+
};
|
|
2106
|
+
const capRefs = caps.map((c) => c.key).sort((a, b) => railOf(a).localeCompare(railOf(b)) || a.localeCompare(b, undefined, { numeric: true }));
|
|
718
2107
|
if (capRefs.length) {
|
|
719
2108
|
// The bank stacks under the circuit at the circuit's own width, the
|
|
720
2109
|
// way a hand-drawn sheet does — a 45-cap ribbon run out to the band
|
|
@@ -722,34 +2111,98 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
722
2111
|
// it deserved (#233). The floor keeps a short bank (four typical cap
|
|
723
2112
|
// cells) on one row even when the circuit above it is narrower.
|
|
724
2113
|
const blockW = Math.max(64, colX - groupX);
|
|
725
|
-
|
|
2114
|
+
let capBudget = Math.min(bandW, blockW);
|
|
2115
|
+
// Under a height budget the bank is part of the group's height too:
|
|
2116
|
+
// when its rows at the circuit's width would run past the budget,
|
|
2117
|
+
// widen the bank — only as far as the rows need, never past the
|
|
2118
|
+
// band — instead of letting it decide the sheet on its own.
|
|
2119
|
+
if (colBudgetH !== Infinity) {
|
|
2120
|
+
const capWidths = capRefs.map((ref) => {
|
|
2121
|
+
const b = bodyBoundsOf(instByKey.get(ref).sym);
|
|
2122
|
+
return ceilU(b.maxX - b.minX) + 2 * MARGIN;
|
|
2123
|
+
});
|
|
2124
|
+
const rowsAt = (budget) => {
|
|
2125
|
+
let rows = 1;
|
|
2126
|
+
let x = 0;
|
|
2127
|
+
for (const w of capWidths) {
|
|
2128
|
+
if (x > 0 && x + w > budget) {
|
|
2129
|
+
rows++;
|
|
2130
|
+
x = 0;
|
|
2131
|
+
}
|
|
2132
|
+
x += w;
|
|
2133
|
+
}
|
|
2134
|
+
return rows;
|
|
2135
|
+
};
|
|
2136
|
+
const rowsAllowed = Math.max(1, Math.floor((colBudgetH - groupMaxY - (MARGIN + 4)) / (2 * MARGIN + 6)));
|
|
2137
|
+
if (rowsAt(capBudget) > rowsAllowed) {
|
|
2138
|
+
const total = capWidths.reduce((a, b) => a + b, 0);
|
|
2139
|
+
capBudget = Math.min(bandW, Math.max(capBudget, Math.ceil(total / rowsAllowed) + Math.max(...capWidths)));
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
726
2142
|
let capX = groupX;
|
|
727
2143
|
let capY = groupMaxY + MARGIN + 4;
|
|
2144
|
+
let bankRowH = 2 * MARGIN + 6;
|
|
2145
|
+
// A bank member with horizontal leads (a TVS diode drawn lying down)
|
|
2146
|
+
// stands up like the caps beside it, its rail lead on top; lying in
|
|
2147
|
+
// the row it carried its ground symbol sideways under the next cap's
|
|
2148
|
+
// name. Each member is turned, and measured, by its own leads.
|
|
2149
|
+
const bankRotOf = (ref) => {
|
|
2150
|
+
const inst = instByKey.get(ref);
|
|
2151
|
+
if (inst.sym.pins.length !== 2 || !inst.sym.pins.every((p) => outward(p).dx !== 0))
|
|
2152
|
+
return 0;
|
|
2153
|
+
const railPin = inst.sym.pins.find((p) => (netClasses.get(netByEndpoint.get(`${inst.ref}.${p.number}`)?.name ?? '')?.cls ?? 'signal') === 'rail') ?? inst.sym.pins[0];
|
|
2154
|
+
return orientFor(ref, railPin.number, { dx: 0, dy: -1 }) ?? 90;
|
|
2155
|
+
};
|
|
728
2156
|
for (const ref of capRefs) {
|
|
729
2157
|
const inst = instByKey.get(ref);
|
|
730
|
-
const
|
|
2158
|
+
const rot = bankRotOf(ref);
|
|
2159
|
+
const sym = rotatedSym(inst.sym, rot);
|
|
2160
|
+
const b = bodyBoundsOf(sym);
|
|
2161
|
+
// a measured cap takes the room its drawing needed (its own fields,
|
|
2162
|
+
// the rail bar and name above, the ground below), plus the pad
|
|
2163
|
+
const mo = measured?.get(ref);
|
|
2164
|
+
const capW = mo ? ceilU(mo.left) + ceilU(b.maxX - b.minX) + ceilU(mo.right) + 2 * MEASURE_PAD : ceilU(b.maxX - b.minX) + 2 * MARGIN;
|
|
2165
|
+
const capLeft = mo ? ceilU(mo.left) + MEASURE_PAD : MARGIN;
|
|
2166
|
+
const capTop = mo ? ceilU(mo.top) + MEASURE_PAD : MARGIN;
|
|
2167
|
+
const capRowH = mo ? capTop + ceilU(b.maxY - b.minY) + ceilU(mo.bottom) + MEASURE_PAD : 2 * MARGIN + 6;
|
|
2168
|
+
bankRowH = Math.max(bankRowH, capRowH);
|
|
731
2169
|
// Banding (#219): a decoupling bank wider than the budget wraps onto
|
|
732
|
-
// another uniform row rather than running past the frame
|
|
733
|
-
|
|
2170
|
+
// another uniform row rather than running past the frame; and it
|
|
2171
|
+
// wraps BEFORE a rail whose caps would not all fit the row, so one
|
|
2172
|
+
// rail's caps stay on one trunk (PVDD's pair split across two rows
|
|
2173
|
+
// left the second cap with its own two symbols).
|
|
2174
|
+
const railStart = capRefs.indexOf(ref) === 0 || railOf(capRefs[capRefs.indexOf(ref) - 1]) !== railOf(ref);
|
|
2175
|
+
let railRunW = 0;
|
|
2176
|
+
if (railStart) {
|
|
2177
|
+
for (let k = capRefs.indexOf(ref); k < capRefs.length && railOf(capRefs[k]) === railOf(ref); k++) {
|
|
2178
|
+
const kb = bodyBoundsOf(rotatedSym(instByKey.get(capRefs[k]).sym, bankRotOf(capRefs[k])));
|
|
2179
|
+
const km = measured?.get(capRefs[k]);
|
|
2180
|
+
railRunW += km ? ceilU(km.left) + ceilU(kb.maxX - kb.minX) + ceilU(km.right) + 2 * MEASURE_PAD : ceilU(kb.maxX - kb.minX) + 2 * MARGIN;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
const needW = railStart && railRunW <= capBudget ? railRunW : capW;
|
|
2184
|
+
if (capX > groupX && capX + needW - groupX > capBudget) {
|
|
734
2185
|
capX = groupX;
|
|
735
|
-
capY +=
|
|
2186
|
+
capY += bankRowH;
|
|
2187
|
+
bankRowH = capRowH;
|
|
736
2188
|
}
|
|
737
|
-
const ox = grid(capX + MARGIN);
|
|
738
|
-
const oy = grid(capY + MARGIN);
|
|
2189
|
+
const ox = mo ? grid(capX + capLeft - Math.floor(b.minX / U)) : grid(capX + MARGIN);
|
|
2190
|
+
const oy = mo ? grid(capY + capTop + Math.ceil(b.maxY / U)) : grid(capY + MARGIN);
|
|
739
2191
|
placed.set(ref, {
|
|
740
2192
|
part: inst.part,
|
|
741
2193
|
refDes: inst.ref,
|
|
742
2194
|
unit: inst.unit,
|
|
743
|
-
sym
|
|
2195
|
+
sym,
|
|
744
2196
|
x: ox,
|
|
745
2197
|
y: oy,
|
|
746
2198
|
body: { minX: ox + b.minX, minY: oy - b.maxY, maxX: ox + b.maxX, maxY: oy - b.minY },
|
|
747
|
-
cellW:
|
|
748
|
-
cellH:
|
|
2199
|
+
cellW: capW,
|
|
2200
|
+
cellH: capRowH,
|
|
2201
|
+
rot,
|
|
749
2202
|
});
|
|
750
|
-
capX +=
|
|
2203
|
+
capX += capW;
|
|
751
2204
|
}
|
|
752
|
-
groupMaxY = capY +
|
|
2205
|
+
groupMaxY = capY + bankRowH;
|
|
753
2206
|
}
|
|
754
2207
|
// ---------- idiom micro-templates and the alignment pass (7.5/7.5a) ----------
|
|
755
2208
|
// Column placement is correct but reads machine-made for the small
|
|
@@ -800,6 +2253,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
800
2253
|
body: { minX: ox + b.minX, minY: oy - b.maxY, maxX: ox + b.maxX, maxY: oy - b.minY },
|
|
801
2254
|
cellW: prev.cellW,
|
|
802
2255
|
cellH: prev.cellH,
|
|
2256
|
+
rot: prev.rot,
|
|
803
2257
|
};
|
|
804
2258
|
};
|
|
805
2259
|
/** Apply candidate moves unless any moved body lands within a grid unit of
|
|
@@ -852,8 +2306,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
852
2306
|
const end = { x: p.x + o.dx * len * U, y: p.y + o.dy * len * U };
|
|
853
2307
|
if (!ownEps.has(ep)) {
|
|
854
2308
|
for (const q of [p, end]) {
|
|
855
|
-
if (sameCoord(q.x, axisX) && q.y > yMin - U && q.y < yMax + U)
|
|
2309
|
+
if (sameCoord(q.x, axisX) && q.y > yMin - U && q.y < yMax + U) {
|
|
2310
|
+
trace(`axis x=${axisX} blocked by ${ep} (${net.name}) at y=${q.y}`);
|
|
856
2311
|
return false;
|
|
2312
|
+
}
|
|
857
2313
|
}
|
|
858
2314
|
}
|
|
859
2315
|
// A horizontal stub SEGMENT crossing the axis exactly at a chain
|
|
@@ -871,8 +2327,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
871
2327
|
if (row && row.net !== net.name) {
|
|
872
2328
|
const lo = Math.min(p.x, end.x) - 0.001;
|
|
873
2329
|
const hi = Math.max(p.x, end.x) + 0.001;
|
|
874
|
-
if (axisX >= lo && axisX <= hi)
|
|
2330
|
+
if (axisX >= lo && axisX <= hi) {
|
|
2331
|
+
trace(`axis x=${axisX} crossed by the stub of ${ep} (${net.name}) on a ${row.net} row`);
|
|
875
2332
|
return false;
|
|
2333
|
+
}
|
|
876
2334
|
}
|
|
877
2335
|
}
|
|
878
2336
|
}
|
|
@@ -880,7 +2338,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
880
2338
|
return true;
|
|
881
2339
|
};
|
|
882
2340
|
/** The room a rail/ground end grows past its pin: stub, bar, value text. */
|
|
883
|
-
const POWER_CLEAR =
|
|
2341
|
+
const POWER_CLEAR = 5 * U;
|
|
884
2342
|
const powerEndBox = (axisX, pinY, dir) => ({
|
|
885
2343
|
minX: axisX - 3 * U,
|
|
886
2344
|
maxX: axisX + 3 * U,
|
|
@@ -890,8 +2348,8 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
890
2348
|
/** Clearance-check a chain (or flank) move set against its axis and its
|
|
891
2349
|
* power-end growth, then apply; marks the parts idiom-placed only when
|
|
892
2350
|
* everything held. */
|
|
893
|
-
const finalizeMoves = (segments, moves, clearBoxes = []) => {
|
|
894
|
-
const own = ownEndpoints(moves.keys());
|
|
2351
|
+
const finalizeMoves = (segments, moves, clearBoxes = [], ownOverride) => {
|
|
2352
|
+
const own = ownOverride ?? ownEndpoints(moves.keys());
|
|
895
2353
|
const movedRefs = new Set(moves.keys());
|
|
896
2354
|
for (const seg of segments) {
|
|
897
2355
|
// the pad covers the power stub and symbol a rail/ground end grows
|
|
@@ -905,8 +2363,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
905
2363
|
for (const [oref, op] of placed) {
|
|
906
2364
|
if (moves.has(oref))
|
|
907
2365
|
continue;
|
|
908
|
-
if (padOverlap(box, op.body, 0))
|
|
2366
|
+
if (padOverlap(box, op.body, 0)) {
|
|
2367
|
+
trace(`power end at x=${(box.minX + box.maxX) / 2} would sit on ${oref}`);
|
|
909
2368
|
return false;
|
|
2369
|
+
}
|
|
910
2370
|
}
|
|
911
2371
|
}
|
|
912
2372
|
if (!applyMoves(moves))
|
|
@@ -956,10 +2416,224 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
956
2416
|
segments.push({ axisX, ys: [at.y, cand.y - v.bot.y] });
|
|
957
2417
|
clearBoxes.push(powerEndBox(axisX, cand.y - v.bot.y, 1)); // the ground symbol below the cap
|
|
958
2418
|
}
|
|
959
|
-
// all-or-nothing per crystal: one dropped cap and one column cap would
|
|
960
|
-
// read worse than the plain columns the pass is improving on
|
|
961
|
-
if (moves.size)
|
|
962
|
-
finalizeMoves(segments, moves, clearBoxes);
|
|
2419
|
+
// all-or-nothing per crystal: one dropped cap and one column cap would
|
|
2420
|
+
// read worse than the plain columns the pass is improving on
|
|
2421
|
+
if (moves.size)
|
|
2422
|
+
finalizeMoves(segments, moves, clearBoxes);
|
|
2423
|
+
}
|
|
2424
|
+
// Pin-anchored hangs claimed above: each chain drops from its pin's row
|
|
2425
|
+
// into its shelf slot. Slot 0 sits past the IC's labels on that side,
|
|
2426
|
+
// so the wire pass draws pin, stub, a short branch and the chain's
|
|
2427
|
+
// stub as one local route; deeper slots stagger outward. A hang the
|
|
2428
|
+
// clearance check refuses goes to the leftover column at the group's
|
|
2429
|
+
// right edge, never on top of something.
|
|
2430
|
+
const leftovers = [];
|
|
2431
|
+
const placeInfill = () => {
|
|
2432
|
+
for (const [host, cells] of infill) {
|
|
2433
|
+
const g = hostGeom.get(host);
|
|
2434
|
+
const d = cellDims.get(host);
|
|
2435
|
+
if (!g)
|
|
2436
|
+
continue;
|
|
2437
|
+
for (const f of cells) {
|
|
2438
|
+
const cd = cellDims.get(f.key);
|
|
2439
|
+
const fb = cd.body;
|
|
2440
|
+
const x0 = f.side === 1 ? g.colX + g.colW - g.colShelfR + (d.reachR ?? 0) : g.colX + 1;
|
|
2441
|
+
const fcx = x0 + Math.floor(cd.w / 2);
|
|
2442
|
+
const fcy = g.rowY + f.relY + Math.floor(cd.h / 2);
|
|
2443
|
+
const fox = grid(fcx - Math.round((fb.minX + fb.maxX) / 2 / U));
|
|
2444
|
+
const foy = grid(fcy + Math.round((fb.minY + fb.maxY) / 2 / U));
|
|
2445
|
+
placed.set(f.key, placeCell(f.key, fox, foy));
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
const placeCell = (key, ox, oy, rot = 0, mirror) => {
|
|
2450
|
+
const inst = instByKey.get(key);
|
|
2451
|
+
const dims = cellDims.get(key);
|
|
2452
|
+
const sym = transformSym(inst.sym, rot, mirror);
|
|
2453
|
+
const b = bodyBoundsOf(sym);
|
|
2454
|
+
return {
|
|
2455
|
+
part: inst.part,
|
|
2456
|
+
refDes: inst.ref,
|
|
2457
|
+
unit: inst.unit,
|
|
2458
|
+
sym,
|
|
2459
|
+
x: ox,
|
|
2460
|
+
y: oy,
|
|
2461
|
+
body: { minX: ox + b.minX, minY: oy - b.maxY, maxX: ox + b.maxX, maxY: oy - b.minY },
|
|
2462
|
+
cellW: dims.w,
|
|
2463
|
+
cellH: dims.h,
|
|
2464
|
+
rot,
|
|
2465
|
+
...(mirror ? { mirror } : {}),
|
|
2466
|
+
};
|
|
2467
|
+
};
|
|
2468
|
+
placeInfill();
|
|
2469
|
+
// Inline runs: each part lies on its pin's row with its near lead
|
|
2470
|
+
// toward the IC, one gap past the stub, the next part one gap on. The
|
|
2471
|
+
// wire pass then draws pin, stub, gap and lead as one straight line.
|
|
2472
|
+
for (const il of inlines) {
|
|
2473
|
+
const icPl = placed.get(il.ic);
|
|
2474
|
+
if (!icPl) {
|
|
2475
|
+
leftovers.push(...il.chain.map((c) => c.key));
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
const at = pinAt(icPl, il.pin);
|
|
2479
|
+
const s = { x: at.x + il.dx * STUB * U, y: at.y };
|
|
2480
|
+
const moves = new Map();
|
|
2481
|
+
let cursor = s.x + il.dx * il.offset; // x of the last connection point on the row
|
|
2482
|
+
let ok = true;
|
|
2483
|
+
for (const c of il.chain) {
|
|
2484
|
+
const ctl = controlPinOf(c.key);
|
|
2485
|
+
// a transistor keeps collector up and emitter down; it is mirrored,
|
|
2486
|
+
// never turned, so its base faces the part that drives it
|
|
2487
|
+
const mirror = ctl ? (outward(ctl).dx === -il.dx ? undefined : 'y') : undefined;
|
|
2488
|
+
const rot = ctl ? 0 : orientFor(c.key, c.nearPin, { dx: -il.dx, dy: 0 });
|
|
2489
|
+
if (rot === null) {
|
|
2490
|
+
ok = false;
|
|
2491
|
+
break;
|
|
2492
|
+
}
|
|
2493
|
+
cursor = cursor + il.dx * c.gap; // the next near lead
|
|
2494
|
+
const rs = transformSym(instByKey.get(c.key).sym, rot, mirror);
|
|
2495
|
+
const near = rs.pins.find((pn) => pn.number === c.nearPin);
|
|
2496
|
+
const ox = grid(Math.round((cursor - near.x) / U));
|
|
2497
|
+
const oy = grid(Math.round((s.y + near.y) / U));
|
|
2498
|
+
moves.set(c.key, placeCell(c.key, ox, oy, rot, mirror));
|
|
2499
|
+
cursor = cursor + il.dx * spanOf(c.key);
|
|
2500
|
+
}
|
|
2501
|
+
if (ok) {
|
|
2502
|
+
for (const [key, cand] of moves)
|
|
2503
|
+
placed.set(key, cand);
|
|
2504
|
+
// no axis to clear: the run's wires are the row itself, which the
|
|
2505
|
+
// wire pass checks segment by segment; bodies must still fit
|
|
2506
|
+
if (!finalizeMoves([], moves, [], new Set())) {
|
|
2507
|
+
for (const key of moves.keys())
|
|
2508
|
+
placed.delete(key);
|
|
2509
|
+
ok = false;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
if (!ok) {
|
|
2513
|
+
leftovers.push(...il.chain.map((c) => c.key));
|
|
2514
|
+
for (const c of il.chain)
|
|
2515
|
+
boundTo.delete(c.key);
|
|
2516
|
+
const refs = il.chain.map((c) => instByKey.get(c.key).ref).join(', ');
|
|
2517
|
+
const note = `inline refused: ${refs} could not lie on ${instByKey.get(il.ic).ref}.${il.pin.number}'s row in "${gname}"; drawn in a column instead`;
|
|
2518
|
+
trace(note);
|
|
2519
|
+
if (!notes.includes(note))
|
|
2520
|
+
notes.push(note);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
for (const hg of hangs) {
|
|
2524
|
+
const icPl = placed.get(hg.ic);
|
|
2525
|
+
if (!icPl) {
|
|
2526
|
+
leftovers.push(...hg.chain);
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
// a node anchor is a hung part, placed turned: read its pin as drawn
|
|
2530
|
+
const pinNow = icPl.sym.pins.find((p) => p.number === hg.pin.number) ?? hg.pin;
|
|
2531
|
+
const at = pinAt(icPl, pinNow);
|
|
2532
|
+
const s = { x: at.x + hg.dx * STUB * U, y: at.y };
|
|
2533
|
+
const side = hangs.filter((o) => o.ic === hg.ic && o.dx === hg.dx);
|
|
2534
|
+
const pitch = Math.max(0, ...side.map((o) => o.w)) + U;
|
|
2535
|
+
const ext = labelExtents([hg.ic]);
|
|
2536
|
+
const reach = sideReach(hg.ic, hg.dx);
|
|
2537
|
+
void ext;
|
|
2538
|
+
const firstSlot = grid(Math.round((s.x + hg.dx * (reach + 2 * U + pitch / 2)) / U));
|
|
2539
|
+
const slotX = grid(Math.round((firstSlot + hg.dx * hg.slot * pitch) / U));
|
|
2540
|
+
trace(`hang ${hg.chain.map((k) => instByKey.get(k).ref).join('+')} on ${instByKey.get(hg.ic).ref}.${hg.pin.number}: stub (${s.x}, ${s.y}) reach ${reach.toFixed(2)} pitch ${pitch.toFixed(2)} slot ${hg.slot} -> x ${hg.straight ? s.x : slotX}${hg.straight ? ' (straight)' : ''}`);
|
|
2541
|
+
const netOfHang = (key, pinN) => netByEndpoint.get(`${instByKey.get(key).ref}.${pinN}`)?.name ?? '';
|
|
2542
|
+
let placedOk = false;
|
|
2543
|
+
// dead straight on the stub axis when the pin-neighbour test found
|
|
2544
|
+
// the rows free (AC-16.31), else the reserved shelf slot. Trying the
|
|
2545
|
+
// axis regardless passed the clearance check — a wire may legally
|
|
2546
|
+
// cross a neighbouring rail stub — and drew exactly that crossing
|
|
2547
|
+
// on the Tier C divider, twice.
|
|
2548
|
+
const axes = hg.straight ? [s.x] : [slotX];
|
|
2549
|
+
// Chains on one side start level: drops below the side's lowest hung
|
|
2550
|
+
// pin row, rises above its highest. Started at its own row, a chain
|
|
2551
|
+
// from one pin put its body across the next pin's row, and that
|
|
2552
|
+
// pin's run out to its lane hit the body (U1's FLT pull-up against
|
|
2553
|
+
// the OVLO divider). Level bases turn that into a wire crossing at
|
|
2554
|
+
// worst, and the resistors of a comb line up the way a drafter
|
|
2555
|
+
// lines them up. A straight hang keeps its own row.
|
|
2556
|
+
const sidePinYsAbs = side.map((o) => pinAt(icPl, icPl.sym.pins.find((p) => p.number === o.pin.number) ?? o.pin).y);
|
|
2557
|
+
const baseY = hg.straight ? s.y : hg.dir === 1 ? Math.max(...sidePinYsAbs) : Math.min(...sidePinYsAbs);
|
|
2558
|
+
for (const axisX of axes) {
|
|
2559
|
+
for (let lift = 0; lift < 3 && !placedOk; lift++) {
|
|
2560
|
+
// the near lead of the first part sits one gap from the base row,
|
|
2561
|
+
// and each further part one gap on, away from the IC
|
|
2562
|
+
let cursor = grid(Math.round((baseY + hg.dir * HANG_GAP) / U)) + hg.dir * lift * 2 * U;
|
|
2563
|
+
const moves = new Map();
|
|
2564
|
+
const conn = [{ y: s.y, net: netOfHang(hg.ic, hg.pin.number) }];
|
|
2565
|
+
const startY = cursor;
|
|
2566
|
+
const nearPins = hangNearPins.get(hg.chain.join('+'));
|
|
2567
|
+
for (const [i, key] of hg.chain.entries()) {
|
|
2568
|
+
// turned so the near lead faces the row (up for a hang that
|
|
2569
|
+
// drops, down for one that rises) and the far lead points away
|
|
2570
|
+
const rot = orientFor(key, nearPins[i], { dx: 0, dy: -hg.dir }) ?? 0;
|
|
2571
|
+
const rs = rotatedSym(instByKey.get(key).sym, rot);
|
|
2572
|
+
const near = rs.pins.find((pn) => pn.number === nearPins[i]);
|
|
2573
|
+
const farLead = rs.pins.find((pn) => pn.number !== nearPins[i]) ?? near;
|
|
2574
|
+
const span = spanOf(key, rot);
|
|
2575
|
+
moves.set(key, placeCell(key, axisX - near.x, cursor + near.y, rot));
|
|
2576
|
+
conn.push({ y: cursor, net: netOfHang(key, near.number) }, { y: cursor + hg.dir * span, net: netOfHang(key, farLead.number) });
|
|
2577
|
+
cursor = cursor + hg.dir * (span + HANG_GAP);
|
|
2578
|
+
}
|
|
2579
|
+
const endY = cursor - hg.dir * HANG_GAP;
|
|
2580
|
+
const clearBoxes = hg.ends === 'power' ? [powerEndBox(axisX, endY, hg.dir)] : [];
|
|
2581
|
+
const ys = [Math.min(startY, endY, s.y), Math.max(startY, endY, s.y)];
|
|
2582
|
+
// Only the nets that run ALONG the axis are the chain's own: the
|
|
2583
|
+
// anchor net and each link between chain parts. The rail or ground
|
|
2584
|
+
// at the far end is not — a pull-up's rail is also the IC's supply
|
|
2585
|
+
// pin, and counting it as own let an axis run straight through
|
|
2586
|
+
// that pin (esp32-amp's BUCK_EN divider on U4's PVDD pin; the
|
|
2587
|
+
// router refused the wire, so the net shipped labelled).
|
|
2588
|
+
const axisNets = new Set(conn.map((c) => c.net));
|
|
2589
|
+
const own = new Set();
|
|
2590
|
+
for (const net of intent.nets)
|
|
2591
|
+
if (axisNets.has(net.name))
|
|
2592
|
+
for (const ep of net.pins)
|
|
2593
|
+
own.add(ep);
|
|
2594
|
+
// the clearance check reads a chain's OWN connection points from the
|
|
2595
|
+
// placed map; hung parts have no column position, so they go in as
|
|
2596
|
+
// their candidates first and come out again if the check refuses
|
|
2597
|
+
for (const [key, cand] of moves)
|
|
2598
|
+
placed.set(key, cand);
|
|
2599
|
+
if (finalizeMoves([{ axisX, ys, conn }], moves, clearBoxes, own))
|
|
2600
|
+
placedOk = true;
|
|
2601
|
+
else
|
|
2602
|
+
for (const key of moves.keys())
|
|
2603
|
+
placed.delete(key);
|
|
2604
|
+
}
|
|
2605
|
+
if (placedOk)
|
|
2606
|
+
break;
|
|
2607
|
+
}
|
|
2608
|
+
if (!placedOk) {
|
|
2609
|
+
leftovers.push(...hg.chain);
|
|
2610
|
+
for (const k of hg.chain)
|
|
2611
|
+
boundTo.delete(k);
|
|
2612
|
+
const refs = hg.chain.map((k) => instByKey.get(k).ref).join(', ');
|
|
2613
|
+
const note = `hang refused: ${refs} could not be placed cleanly on ${instByKey.get(hg.ic).ref}.${hg.pin.number} in "${gname}"; drawn in a column instead`;
|
|
2614
|
+
trace(note);
|
|
2615
|
+
if (!notes.includes(note))
|
|
2616
|
+
notes.push(note);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (leftovers.length) {
|
|
2620
|
+
// a column of their own past everything the group placed so far
|
|
2621
|
+
let x = Math.max(groupX, ...[...placed.values()].filter((p) => groupOf.get([...placed.entries()].find(([, q]) => q === p)?.[0] ?? '') === gname).map((p) => Math.ceil(p.body.maxX / U) + MARGIN)) + CHANNEL;
|
|
2622
|
+
const colW = Math.max(...leftovers.map((k) => cellDims.get(k).w));
|
|
2623
|
+
let rowY = bandTop;
|
|
2624
|
+
for (const key of leftovers) {
|
|
2625
|
+
const dims = cellDims.get(key);
|
|
2626
|
+
const b = dims.body;
|
|
2627
|
+
const cx = x + Math.floor(colW / 2);
|
|
2628
|
+
const cy = rowY + Math.floor(dims.h / 2);
|
|
2629
|
+
const ox = grid(cx - Math.round((b.minX + b.maxX) / 2 / U));
|
|
2630
|
+
const oy = grid(cy + Math.round((b.minY + b.maxY) / 2 / U));
|
|
2631
|
+
placed.set(key, placeCell(key, ox, oy));
|
|
2632
|
+
rowY += dims.h + ROW_GAP;
|
|
2633
|
+
}
|
|
2634
|
+
groupMaxY = Math.max(groupMaxY, rowY - ROW_GAP);
|
|
2635
|
+
colX = x + colW + CHANNEL;
|
|
2636
|
+
void colX;
|
|
963
2637
|
}
|
|
964
2638
|
const walk = (start, dir, chain) => {
|
|
965
2639
|
let current = start;
|
|
@@ -971,14 +2645,22 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
971
2645
|
return { kind: 'open' }; // declared no-connect or unused
|
|
972
2646
|
if (classOf(net.name) !== 'signal')
|
|
973
2647
|
return { kind: 'power' };
|
|
2648
|
+
// a tapped node is not a series link, but it is a fine END: the chain
|
|
2649
|
+
// stops there and the label that names the node sits on it (R18 over
|
|
2650
|
+
// Q1's drain, its other end on the four-way USB_EN)
|
|
974
2651
|
if (net.pins.length !== 2)
|
|
975
|
-
return { kind: '
|
|
2652
|
+
return { kind: 'open' };
|
|
976
2653
|
const otherEp = net.pins.map(parseEp).find((e) => e !== null && e.key !== current);
|
|
977
2654
|
if (!otherEp)
|
|
978
2655
|
return { kind: 'invalid' };
|
|
979
2656
|
const opl = placed.get(otherEp.key);
|
|
980
|
-
if (!opl
|
|
2657
|
+
if (!opl)
|
|
981
2658
|
return { kind: 'invalid' };
|
|
2659
|
+
// the other end lives in another group: this end is open, and the
|
|
2660
|
+
// label that names the net across the sheet sits on it (an LED's
|
|
2661
|
+
// resistor driven from the MCU group stacks over its LED here)
|
|
2662
|
+
if (groupOf.get(otherEp.key) !== gname)
|
|
2663
|
+
return { kind: 'open' };
|
|
982
2664
|
if (chainable(otherEp.key) && !chain.includes(otherEp.key)) {
|
|
983
2665
|
const ov = vertPins(otherEp.key);
|
|
984
2666
|
// the link must enter through the lead facing the chain, or the
|
|
@@ -1003,12 +2685,33 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1003
2685
|
if (!chainable(m.key) || chained.has(m.key))
|
|
1004
2686
|
continue;
|
|
1005
2687
|
const chain = [m.key];
|
|
1006
|
-
|
|
1007
|
-
|
|
2688
|
+
let topEnd = walk(m.key, 'up', chain);
|
|
2689
|
+
let bottomEnd = walk(chain[chain.length - 1], 'down', chain);
|
|
1008
2690
|
for (const ref of chain)
|
|
1009
2691
|
chained.add(ref);
|
|
2692
|
+
trace(`chain ${chain.map((k) => instByKey.get(k).ref).join('+')}: top ${topEnd.kind} bottom ${bottomEnd.kind}`);
|
|
1010
2693
|
if (topEnd.kind === 'invalid' || bottomEnd.kind === 'invalid')
|
|
1011
2694
|
continue;
|
|
2695
|
+
// The anchor sits BELOW the chain but meets its top lead (R18's pin 1
|
|
2696
|
+
// on Q1's drain, which faces up): turn every part half a turn so the
|
|
2697
|
+
// lead that meets the anchor is the bottom one, and stack upward.
|
|
2698
|
+
// The reverse case (an anchor above, met by a bottom lead) mirrors it.
|
|
2699
|
+
// A turned chain that is then abandoned below gets its column's
|
|
2700
|
+
// orientation back: `abandon` restores every part the turn replaced.
|
|
2701
|
+
const unturned = new Map();
|
|
2702
|
+
const flip = () => {
|
|
2703
|
+
for (const key of chain) {
|
|
2704
|
+
const pl = placed.get(key);
|
|
2705
|
+
unturned.set(key, pl);
|
|
2706
|
+
placed.set(key, placeCell(key, pl.x, pl.y, (pl.rot + 180) % 360, pl.mirror));
|
|
2707
|
+
}
|
|
2708
|
+
[topEnd, bottomEnd] = [bottomEnd, topEnd];
|
|
2709
|
+
chain.reverse();
|
|
2710
|
+
};
|
|
2711
|
+
const abandon = () => {
|
|
2712
|
+
for (const [key, pl] of unturned)
|
|
2713
|
+
placed.set(key, pl);
|
|
2714
|
+
};
|
|
1012
2715
|
if (chain.length > 4)
|
|
1013
2716
|
continue; // beyond four parts this is a network, not an idiom
|
|
1014
2717
|
const anchors = [topEnd, bottomEnd].filter((e) => e.kind === 'anchor');
|
|
@@ -1016,6 +2719,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1016
2719
|
continue; // a lone floating part has nothing to align to
|
|
1017
2720
|
if (topEnd.kind === 'open' && bottomEnd.kind === 'open')
|
|
1018
2721
|
continue;
|
|
2722
|
+
if (topEnd.kind === 'anchor' && outward(topEnd.pin).dy === -1 && bottomEnd.kind !== 'anchor')
|
|
2723
|
+
flip();
|
|
2724
|
+
else if (bottomEnd.kind === 'anchor' && outward(bottomEnd.pin).dy === 1 && topEnd.kind !== 'anchor')
|
|
2725
|
+
flip();
|
|
1019
2726
|
const stubEndOf = (a) => {
|
|
1020
2727
|
const at = pinAt(placed.get(a.ref), a.pin);
|
|
1021
2728
|
const o = outward(a.pin);
|
|
@@ -1026,14 +2733,18 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1026
2733
|
let order = chain;
|
|
1027
2734
|
if (topEnd.kind === 'anchor') {
|
|
1028
2735
|
const s = stubEndOf(topEnd);
|
|
1029
|
-
if (s.o.dy === -1)
|
|
1030
|
-
|
|
2736
|
+
if (s.o.dy === -1) {
|
|
2737
|
+
abandon(); // an up-facing pin cannot feed a downward run
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
1031
2740
|
if (bottomEnd.kind === 'anchor') {
|
|
1032
2741
|
const b = stubEndOf(bottomEnd);
|
|
1033
2742
|
// both ends must sit on one axis with the second anchor below and
|
|
1034
2743
|
// able to receive from above, else leave the columns alone
|
|
1035
|
-
if (!sameCoord(s.x, b.x) || b.y <= s.y || b.o.dy === 1)
|
|
2744
|
+
if (!sameCoord(s.x, b.x) || b.y <= s.y || b.o.dy === 1) {
|
|
2745
|
+
abandon();
|
|
1036
2746
|
continue;
|
|
2747
|
+
}
|
|
1037
2748
|
}
|
|
1038
2749
|
axisX = s.x;
|
|
1039
2750
|
cursor = s.y + CHAIN_GAP;
|
|
@@ -1041,14 +2752,17 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1041
2752
|
else if (bottomEnd.kind === 'anchor') {
|
|
1042
2753
|
// rail above, anchor below (a pull-up): stack upward from the anchor
|
|
1043
2754
|
const s = stubEndOf(bottomEnd);
|
|
1044
|
-
if (s.o.dy === 1)
|
|
1045
|
-
|
|
2755
|
+
if (s.o.dy === 1) {
|
|
2756
|
+
abandon(); // a down-facing pin cannot feed an upward run
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
1046
2759
|
axisX = s.x;
|
|
1047
2760
|
order = [...chain].reverse();
|
|
1048
2761
|
// Bounded lift: when a connection row would sit on a foreign stub's
|
|
1049
2762
|
// crossing (axisClear's segment check), raise the whole stack a grid
|
|
1050
2763
|
// row at a time rather than shipping the contact or losing the idiom.
|
|
1051
2764
|
const netOf = (key, pinN) => netByEndpoint.get(epOf(key, pinN))?.name ?? '';
|
|
2765
|
+
let lifted = false;
|
|
1052
2766
|
for (let lift = 0; lift < 3; lift++) {
|
|
1053
2767
|
let up = s.y - CHAIN_GAP - lift * 2 * U;
|
|
1054
2768
|
const moves = new Map();
|
|
@@ -1064,9 +2778,13 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1064
2778
|
const topY = up + CHAIN_GAP;
|
|
1065
2779
|
const topRef = order[order.length - 1];
|
|
1066
2780
|
const done = finalizeMoves([{ axisX, ys: [s.y, topY], conn: [...conn, { y: topY, net: netOf(topRef, vertPins(topRef).top.number) }] }], moves, topEnd.kind === 'power' ? [powerEndBox(axisX, topY, -1)] : []);
|
|
1067
|
-
if (done)
|
|
2781
|
+
if (done) {
|
|
2782
|
+
lifted = true;
|
|
1068
2783
|
break;
|
|
2784
|
+
}
|
|
1069
2785
|
}
|
|
2786
|
+
if (!lifted)
|
|
2787
|
+
abandon();
|
|
1070
2788
|
continue;
|
|
1071
2789
|
}
|
|
1072
2790
|
else {
|
|
@@ -1079,6 +2797,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1079
2797
|
}
|
|
1080
2798
|
const cursor0 = cursor;
|
|
1081
2799
|
const netOf2 = (key, pinN) => netByEndpoint.get(epOf(key, pinN))?.name ?? '';
|
|
2800
|
+
let stacked = false;
|
|
1082
2801
|
for (let lift = 0; lift < 3; lift++) {
|
|
1083
2802
|
cursor = cursor0 + lift * 2 * U;
|
|
1084
2803
|
const moves = new Map();
|
|
@@ -1109,20 +2828,39 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1109
2828
|
const endNet = conn.length ? conn[0].net : '';
|
|
1110
2829
|
const startConn = { y: cursor0 - CHAIN_GAP, net: topEnd.kind === 'anchor' ? netOf2(topEnd.ref, topEnd.pin.number) : endNet };
|
|
1111
2830
|
const lastConn = { y: axisEndY, net: bottomEnd.kind === 'anchor' ? netOf2(bottomEnd.ref, bottomEnd.pin.number) : (conn.length ? conn[conn.length - 1].net : '') };
|
|
1112
|
-
if (fits && finalizeMoves([{ axisX, ys: [cursor0 - CHAIN_GAP, axisEndY], conn: [startConn, ...conn, lastConn] }], moves, clearBoxes))
|
|
2831
|
+
if (fits && finalizeMoves([{ axisX, ys: [cursor0 - CHAIN_GAP, axisEndY], conn: [startConn, ...conn, lastConn] }], moves, clearBoxes)) {
|
|
2832
|
+
stacked = true;
|
|
1113
2833
|
break;
|
|
2834
|
+
}
|
|
1114
2835
|
if (!fits)
|
|
1115
2836
|
break; // lifting only shrinks the room below; no retry can help
|
|
1116
2837
|
}
|
|
2838
|
+
if (!stacked)
|
|
2839
|
+
abandon();
|
|
1117
2840
|
}
|
|
1118
2841
|
const memberRefs = [...members.map((m) => m.key), ...capRefs];
|
|
1119
2842
|
const cells = memberRefs.map((r) => placed.get(r));
|
|
1120
2843
|
if (cells.length) {
|
|
1121
2844
|
const minX = Math.min(...cells.map((c) => c.body.minX)) - MARGIN * U;
|
|
1122
|
-
|
|
1123
|
-
|
|
2845
|
+
// a two-lead part carries its reference and value beside its body;
|
|
2846
|
+
// at the group's right edge that text is what the next group must
|
|
2847
|
+
// clear (the rails bank's "47uF/10V" ran under the amplifier's box)
|
|
2848
|
+
const fieldReach = (c) => c.sym.pins.length <= 2 ? Math.max(c.refDes.length, c.part.value.length) * TEXT_RESERVE * LABEL_HEIGHT + 1.27 : 0;
|
|
2849
|
+
const maxX = Math.max(...cells.map((c) => c.body.maxX + fieldReach(c))) + MARGIN * U;
|
|
2850
|
+
// the top inset holds the caption band (a bold caption at
|
|
2851
|
+
// CAPTION_SIZE, inset 2 mm) above the tallest thing a cell can carry
|
|
2852
|
+
// over its body: a rail symbol on an upward stub with its value text
|
|
2853
|
+
// over it, which with an IC at the top-left corner sat 4.3 mm under
|
|
2854
|
+
// the box edge at six units; CAPTION_BAND keeps the caption's
|
|
2855
|
+
// bottom edge clear of it
|
|
2856
|
+
const minY = Math.min(...cells.map((c) => c.body.minY)) - (MARGIN + CAPTION_BAND) * U;
|
|
1124
2857
|
const maxY = Math.max(...cells.map((c) => c.body.maxY)) + (MARGIN + 2) * U;
|
|
1125
|
-
|
|
2858
|
+
// a measured round already knows how far the box will grow above
|
|
2859
|
+
// and below its cells (caption band, text, power symbols): the rect
|
|
2860
|
+
// carries it so every fit stacks boxes, not cells
|
|
2861
|
+
const reach = reachMeasured?.get(gname);
|
|
2862
|
+
groupRects.push({ name: gname, x1: minX, y1: minY - (reach?.top ?? 0), x2: maxX, y2: maxY + (reach?.bottom ?? 0) });
|
|
2863
|
+
trace(`group "${gname}": rect x ${minX.toFixed(1)}..${maxX.toFixed(1)} y ${minY.toFixed(1)}..${maxY.toFixed(1)} (${cells.length} cells)`);
|
|
1126
2864
|
groupX = Math.round(maxX / U) + GROUP_GAP;
|
|
1127
2865
|
prevGroup = gname;
|
|
1128
2866
|
bandsOf.set(gname, bandCount);
|
|
@@ -1147,9 +2885,9 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1147
2885
|
const hinted = paperHint ? PAPERS.find((p) => p.name === paperHint) : undefined;
|
|
1148
2886
|
// A hint pins the width budget; otherwise try every sheet, smallest first.
|
|
1149
2887
|
const candidates = hinted ? [hinted] : PAPERS;
|
|
1150
|
-
const gap = GROUP_GAP * U;
|
|
1151
|
-
const usableW = (p) => p.w - 2 * FRAME;
|
|
1152
|
-
const usableH = (p) => p.h - 2 * FRAME - TITLE_STRIP;
|
|
2888
|
+
const gap = GROUP_GAP * U + wrapGap;
|
|
2889
|
+
const usableW = (p) => p.w - 2 * FRAME - frameSlack;
|
|
2890
|
+
const usableH = (p) => p.h - 2 * FRAME - TITLE_STRIP - frameSlack;
|
|
1153
2891
|
/**
|
|
1154
2892
|
* Shelf-wrap the group rects to a width budget; returns per-group offsets.
|
|
1155
2893
|
*
|
|
@@ -1172,22 +2910,286 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1172
2910
|
let rowLeftExt = leftExtOf(groupRects[0].name);
|
|
1173
2911
|
let dyUnits = 0;
|
|
1174
2912
|
let rowH = 0;
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
2913
|
+
// Every box in a row shares the row's top. The single-row pass gives the
|
|
2914
|
+
// boxes different tops when one reserved room above its IC for a
|
|
2915
|
+
// pull-up (esp32-amp's rails box started 16 mm below its neighbours),
|
|
2916
|
+
// and a row height measured box by box then misses that offset, so the
|
|
2917
|
+
// next row landed on the tall box's bottom. Each box moves to the common
|
|
2918
|
+
// top; its height is then its own.
|
|
2919
|
+
const topY = Math.min(...groupRects.map((r) => r.y1));
|
|
2920
|
+
// Rows are consecutive runs of groups (reading order is kept), but WHICH
|
|
2921
|
+
// consecutive runs is chosen to minimise the stack's height, not by
|
|
2922
|
+
// filling each row until the next group no longer fits: first-fit paired
|
|
2923
|
+
// esp32-amp's two tallest groups with two short ones and stacked five
|
|
2924
|
+
// rows where four would do. A group wider than the whole budget still
|
|
2925
|
+
// starts its own row; it will overflow, and the caller rejects this paper
|
|
2926
|
+
// for it.
|
|
2927
|
+
const n = groupRects.length;
|
|
2928
|
+
const heightOf = (r) => r.y2 - r.y1;
|
|
2929
|
+
const runFits = (i, j) => i === j || groupRects[j].x2 - groupRects[i].x1 + leftExtOf(groupRects[i].name) + rightExtOf(groupRects[j].name) <= budgetW;
|
|
2930
|
+
const best = new Array(n + 1).fill(Infinity);
|
|
2931
|
+
const cut = new Array(n + 1).fill(0);
|
|
2932
|
+
best[0] = 0;
|
|
2933
|
+
for (let j = 1; j <= n; j++) {
|
|
2934
|
+
let rowMax = 0;
|
|
2935
|
+
for (let i = j; i >= 1; i--) {
|
|
2936
|
+
rowMax = Math.max(rowMax, heightOf(groupRects[i - 1]));
|
|
2937
|
+
if (!runFits(i - 1, j - 1))
|
|
2938
|
+
break;
|
|
2939
|
+
const h = best[i - 1] + rowMax + (i > 1 ? gap : 0);
|
|
2940
|
+
if (h < best[j]) {
|
|
2941
|
+
best[j] = h;
|
|
2942
|
+
cut[j] = i - 1;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
if (best[j] === Infinity) {
|
|
2946
|
+
// no run ending here fits: the group is its own (overflowing) row
|
|
2947
|
+
best[j] = best[j - 1] + heightOf(groupRects[j - 1]) + (j > 1 ? gap : 0);
|
|
2948
|
+
cut[j] = j - 1;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const starts = [];
|
|
2952
|
+
for (let j = n; j > 0; j = cut[j])
|
|
2953
|
+
starts.unshift(cut[j]);
|
|
2954
|
+
const rowStart = new Set(starts);
|
|
2955
|
+
if (process.env['COPPERHEAD_DRAFT_TRACE'] === '1') {
|
|
2956
|
+
const rows = [];
|
|
2957
|
+
for (let k = 0; k < starts.length; k++) {
|
|
2958
|
+
const a = starts[k];
|
|
2959
|
+
const b = k + 1 < starts.length ? starts[k + 1] : n;
|
|
2960
|
+
const members = groupRects.slice(a, b);
|
|
2961
|
+
rows.push(`[${members.map((r) => r.name.slice(0, 2).trim()).join('+')} h${Math.max(...members.map(heightOf)).toFixed(0)}]`);
|
|
2962
|
+
}
|
|
2963
|
+
trace(`wrap at ${budgetW.toFixed(0)}: rows ${rows.join(' ')} -> ${best[n].toFixed(0)} tall`);
|
|
2964
|
+
}
|
|
2965
|
+
for (const [idx, r] of groupRects.entries()) {
|
|
2966
|
+
if (idx > 0 && rowStart.has(idx)) {
|
|
1179
2967
|
dyUnits += Math.ceil((rowH + gap) / U);
|
|
1180
2968
|
rowOriginX = r.x1;
|
|
1181
2969
|
rowLeftExt = leftExtOf(r.name);
|
|
1182
2970
|
rowH = 0;
|
|
1183
2971
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
2972
|
+
const lift = grid(Math.round((topY - r.y1) / U));
|
|
2973
|
+
deltas.push({ dx: grid(Math.round((originX - rowOriginX) / U)), dy: dyUnits * U + lift });
|
|
2974
|
+
rowH = Math.max(rowH, r.y2 + lift - topY);
|
|
1186
2975
|
}
|
|
2976
|
+
void rowLeftExt;
|
|
1187
2977
|
const xs = groupRects.flatMap((r, i) => [r.x1 + deltas[i].dx, r.x2 + deltas[i].dx]);
|
|
1188
2978
|
const ys = groupRects.flatMap((r, i) => [r.y1 + deltas[i].dy, r.y2 + deltas[i].dy]);
|
|
1189
2979
|
return { deltas, w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) };
|
|
1190
2980
|
};
|
|
2981
|
+
/**
|
|
2982
|
+
* The column-major counterpart of `wrapTo`: groups keep their declared
|
|
2983
|
+
* order but fill top-to-bottom, then left-to-right, the way a person lays
|
|
2984
|
+
* a newspaper page. Consecutive runs form columns no taller than `budgetH`;
|
|
2985
|
+
* a column is as wide as its widest group (label extents included) and the
|
|
2986
|
+
* breaks are chosen to minimise the total width. Groups of like width stack
|
|
2987
|
+
* well this way (the engagement sheet's three 250 mm groups in one column,
|
|
2988
|
+
* its three 205 mm groups in the next), which rows of mismatched heights
|
|
2989
|
+
* never achieve.
|
|
2990
|
+
*/
|
|
2991
|
+
const wrapColumns = (budgetH) => {
|
|
2992
|
+
const originX = groupRects[0].x1;
|
|
2993
|
+
const topY = Math.min(...groupRects.map((r) => r.y1));
|
|
2994
|
+
const leftExtOf = (name) => groupExtents.get(name)?.left ?? 0;
|
|
2995
|
+
const rightExtOf = (name) => groupExtents.get(name)?.right ?? 0;
|
|
2996
|
+
const gap = GROUP_GAP * U + wrapGap;
|
|
2997
|
+
const n = groupRects.length;
|
|
2998
|
+
const hOf = (r) => r.y2 - r.y1;
|
|
2999
|
+
const wOf = (r) => r.x2 - r.x1 + leftExtOf(r.name) + rightExtOf(r.name);
|
|
3000
|
+
const runH = (i, j) => {
|
|
3001
|
+
let h = 0;
|
|
3002
|
+
for (let k = i; k <= j; k++)
|
|
3003
|
+
h += hOf(groupRects[k]) + (k > i ? gap : 0);
|
|
3004
|
+
return h;
|
|
3005
|
+
};
|
|
3006
|
+
const best = new Array(n + 1).fill(Infinity);
|
|
3007
|
+
const cut = new Array(n + 1).fill(0);
|
|
3008
|
+
best[0] = 0;
|
|
3009
|
+
for (let j = 1; j <= n; j++) {
|
|
3010
|
+
let colW = 0;
|
|
3011
|
+
for (let i = j; i >= 1; i--) {
|
|
3012
|
+
colW = Math.max(colW, wOf(groupRects[i - 1]));
|
|
3013
|
+
if (i < j && runH(i - 1, j - 1) > budgetH)
|
|
3014
|
+
break;
|
|
3015
|
+
const w = best[i - 1] + colW + (i > 1 ? gap : 0);
|
|
3016
|
+
if (w < best[j]) {
|
|
3017
|
+
best[j] = w;
|
|
3018
|
+
cut[j] = i - 1;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
const starts = [];
|
|
3023
|
+
for (let j = n; j > 0; j = cut[j])
|
|
3024
|
+
starts.unshift(cut[j]);
|
|
3025
|
+
const deltas = [];
|
|
3026
|
+
let colOriginUnits = Math.round(originX / U);
|
|
3027
|
+
let maxH = 0;
|
|
3028
|
+
for (let k = 0; k < starts.length; k++) {
|
|
3029
|
+
const a = starts[k];
|
|
3030
|
+
const b = k + 1 < starts.length ? starts[k + 1] : n;
|
|
3031
|
+
const members = groupRects.slice(a, b);
|
|
3032
|
+
const colLeft = Math.max(...members.map((r) => leftExtOf(r.name)));
|
|
3033
|
+
let yUnits = Math.round(topY / U);
|
|
3034
|
+
for (const r of members) {
|
|
3035
|
+
deltas.push({ dx: (colOriginUnits + Math.ceil(colLeft / U)) * U - grid(Math.round(r.x1 / U)), dy: yUnits * U - grid(Math.round(r.y1 / U)) });
|
|
3036
|
+
yUnits += Math.ceil(hOf(r) / U) + GROUP_GAP + Math.ceil(wrapGap / U);
|
|
3037
|
+
}
|
|
3038
|
+
maxH = Math.max(maxH, (yUnits - GROUP_GAP) * U - topY);
|
|
3039
|
+
colOriginUnits += Math.ceil(Math.max(...members.map(wOf)) / U) + GROUP_GAP + Math.ceil(wrapGap / U);
|
|
3040
|
+
}
|
|
3041
|
+
if (process.env['COPPERHEAD_DRAFT_TRACE'] === '1') {
|
|
3042
|
+
trace(`column wrap at ${budgetH.toFixed(0)}: ${starts.map((a, k) => `[${groupRects.slice(a, k + 1 < starts.length ? starts[k + 1] : n).map((r) => r.name.slice(0, 2).trim()).join('+')}]`).join(' ')} -> ${best[n].toFixed(0)} wide`);
|
|
3043
|
+
}
|
|
3044
|
+
return { deltas, w: best[n], h: maxH, kind: 'columns' };
|
|
3045
|
+
};
|
|
3046
|
+
/**
|
|
3047
|
+
* The grid a person lays when neither rows nor columns fit: k columns,
|
|
3048
|
+
* groups dealt to them in declared order (group i in column i mod k), each
|
|
3049
|
+
* column stacking its own groups from the top. Reading order runs along
|
|
3050
|
+
* the rows as before, but a short group no longer holds a whole row's
|
|
3051
|
+
* height open beside a tall one (the hand-laid A2 sheet: power, amplifier
|
|
3052
|
+
* and UI in one column, PD, MCU and UART in the next, rails in the third).
|
|
3053
|
+
*/
|
|
3054
|
+
/** Masonry deals already searched in this draft, by budget and group sizes. */
|
|
3055
|
+
const masonryDeals = new Map();
|
|
3056
|
+
const wrapMasonry = (k, budgetH) => {
|
|
3057
|
+
const originX = groupRects[0].x1;
|
|
3058
|
+
const topY = Math.min(...groupRects.map((r) => r.y1));
|
|
3059
|
+
const leftExtOf = (name) => groupExtents.get(name)?.left ?? 0;
|
|
3060
|
+
const rightExtOf = (name) => groupExtents.get(name)?.right ?? 0;
|
|
3061
|
+
const n = groupRects.length;
|
|
3062
|
+
const wOf = (i) => groupRects[i].x2 - groupRects[i].x1 + leftExtOf(groupRects[i].name) + rightExtOf(groupRects[i].name);
|
|
3063
|
+
const hOf = (i) => groupRects[i].y2 - groupRects[i].y1;
|
|
3064
|
+
const gap = GROUP_GAP * U + wrapGap;
|
|
3065
|
+
// Every way of dealing n groups to k columns (order kept within a
|
|
3066
|
+
// column, columns in the order their first groups are declared) is
|
|
3067
|
+
// judged and the narrowest that fits the height wins; ties go to the
|
|
3068
|
+
// shorter. Seven groups in three columns is 301 deals, but twelve in four
|
|
3069
|
+
// is 611 501, and the fit asks for this at every budget of every sheet:
|
|
3070
|
+
// enumerating them all made a twelve-group board draft in tens of
|
|
3071
|
+
// seconds. So the deals are walked as a tree, a group at a time, and a
|
|
3072
|
+
// branch is cut as soon as a column is already too tall, or the columns
|
|
3073
|
+
// opened so far are already wider than the best deal found (or as wide
|
|
3074
|
+
// and as tall): no deal below it could replace the best. The walk visits
|
|
3075
|
+
// deals in the enumeration's order and cuts only branches that could not
|
|
3076
|
+
// win, so the deal it keeps is the one the full enumeration keeps. Past
|
|
3077
|
+
// a node budget the walk stops with the best deal so far, and deals
|
|
3078
|
+
// round-robin when it has found none. Shapes recur across the budgets
|
|
3079
|
+
// the fit tries, so a search is remembered by the groups' sizes.
|
|
3080
|
+
const sizesKey = `${k}|${budgetH}|${gap}|${groupRects.map((_, i) => `${wOf(i)}x${hOf(i)}`).join(',')}`;
|
|
3081
|
+
let cols = masonryDeals.get(sizesKey);
|
|
3082
|
+
if (cols === undefined) {
|
|
3083
|
+
let found = null;
|
|
3084
|
+
let bestW = Infinity;
|
|
3085
|
+
let bestH = Infinity;
|
|
3086
|
+
const judge = (assign) => {
|
|
3087
|
+
const cs = Array.from({ length: k }, () => []);
|
|
3088
|
+
assign.forEach((c, i) => cs[c].push(i));
|
|
3089
|
+
if (cs.some((c) => !c.length))
|
|
3090
|
+
return;
|
|
3091
|
+
const hs = cs.map((c) => c.reduce((h, i, m) => h + hOf(i) + (m ? gap : 0), 0));
|
|
3092
|
+
const maxH = Math.max(...hs);
|
|
3093
|
+
if (maxH > budgetH)
|
|
3094
|
+
return;
|
|
3095
|
+
const w = cs.reduce((acc, c) => acc + Math.max(...c.map(wOf)), 0) + (k - 1) * gap;
|
|
3096
|
+
if (w < bestW - 1e-6 || (Math.abs(w - bestW) <= 1e-6 && maxH < bestH)) {
|
|
3097
|
+
bestW = w;
|
|
3098
|
+
bestH = maxH;
|
|
3099
|
+
found = cs;
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
// columns are opened in reading order: the first group starts the
|
|
3103
|
+
// first column, and a group may start a new column only when every
|
|
3104
|
+
// earlier one is open (a restricted-growth string), so the same deal
|
|
3105
|
+
// is never judged under k! column orders
|
|
3106
|
+
const assign = new Array(n).fill(0);
|
|
3107
|
+
const colH = new Array(k).fill(0);
|
|
3108
|
+
const colW = new Array(k).fill(0);
|
|
3109
|
+
const colN = new Array(k).fill(0);
|
|
3110
|
+
let nodes = 0;
|
|
3111
|
+
const grow = (i, open, widthSoFar, tallest) => {
|
|
3112
|
+
if (nodes > MASONRY_NODE_BUDGET)
|
|
3113
|
+
return;
|
|
3114
|
+
nodes++;
|
|
3115
|
+
if (n - i < k - open)
|
|
3116
|
+
return; // too few groups left to open every column
|
|
3117
|
+
if (i === n) {
|
|
3118
|
+
judge(assign);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
for (let c = 0; c < Math.min(open + 1, k); c++) {
|
|
3122
|
+
const h = colH[c] + hOf(i) + (colN[c] ? gap : 0);
|
|
3123
|
+
if (h > budgetH)
|
|
3124
|
+
continue;
|
|
3125
|
+
const w = Math.max(colW[c], wOf(i));
|
|
3126
|
+
const lowerW = widthSoFar - colW[c] + w + (k - 1) * gap;
|
|
3127
|
+
const tall = Math.max(tallest, h);
|
|
3128
|
+
if (lowerW > bestW + 1e-6 || (lowerW >= bestW - 1e-6 && tall >= bestH))
|
|
3129
|
+
continue;
|
|
3130
|
+
const [h0, w0] = [colH[c], colW[c]];
|
|
3131
|
+
assign[i] = c;
|
|
3132
|
+
colH[c] = h;
|
|
3133
|
+
colW[c] = w;
|
|
3134
|
+
colN[c]++;
|
|
3135
|
+
grow(i + 1, Math.max(open, c + 1), lowerW - (k - 1) * gap, tall);
|
|
3136
|
+
colN[c]--;
|
|
3137
|
+
colH[c] = h0;
|
|
3138
|
+
colW[c] = w0;
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
grow(0, 0, 0, 0);
|
|
3142
|
+
if (nodes > MASONRY_NODE_BUDGET) {
|
|
3143
|
+
trace(`masonry k=${k} at ${budgetH.toFixed(0)}: search stopped at ${MASONRY_NODE_BUDGET} nodes, ${found ? 'keeping the best deal so far' : 'dealing round-robin'}`);
|
|
3144
|
+
if (!found)
|
|
3145
|
+
judge(groupRects.map((_, i) => i % k));
|
|
3146
|
+
}
|
|
3147
|
+
cols = found;
|
|
3148
|
+
masonryDeals.set(sizesKey, cols);
|
|
3149
|
+
}
|
|
3150
|
+
if (!cols)
|
|
3151
|
+
return null;
|
|
3152
|
+
const colW = cols.map((c) => Math.max(0, ...c.map(wOf)));
|
|
3153
|
+
const colH = cols.map((c) => c.reduce((h, i, m) => h + hOf(i) + (m ? gap : 0), 0));
|
|
3154
|
+
const deltas = new Array(groupRects.length);
|
|
3155
|
+
let colOriginUnits = Math.round(originX / U);
|
|
3156
|
+
for (const [ci, c] of cols.entries()) {
|
|
3157
|
+
const colLeft = Math.max(0, ...c.map((i) => leftExtOf(groupRects[i].name)));
|
|
3158
|
+
let yUnits = Math.round(topY / U);
|
|
3159
|
+
for (const i of c) {
|
|
3160
|
+
const r = groupRects[i];
|
|
3161
|
+
deltas[i] = { dx: (colOriginUnits + Math.ceil(colLeft / U)) * U - grid(Math.round(r.x1 / U)), dy: yUnits * U - grid(Math.round(r.y1 / U)) };
|
|
3162
|
+
yUnits += Math.ceil((r.y2 - r.y1) / U) + GROUP_GAP + Math.ceil(wrapGap / U);
|
|
3163
|
+
}
|
|
3164
|
+
colOriginUnits += Math.ceil(colW[ci] / U) + (ci + 1 < k ? GROUP_GAP + Math.ceil(wrapGap / U) : 0);
|
|
3165
|
+
}
|
|
3166
|
+
const w = (colOriginUnits - Math.round(originX / U)) * U;
|
|
3167
|
+
trace(`masonry wrap k=${k} at ${budgetH.toFixed(0)}: ${cols.map((c) => `[${c.map((i) => groupRects[i].name.slice(0, 2).trim()).join('+')}]`).join(' ')} -> ${w.toFixed(0)} wide, ${Math.max(...colH).toFixed(0)} tall`);
|
|
3168
|
+
return { deltas, w, h: Math.max(...colH), kind: 'masonry' };
|
|
3169
|
+
};
|
|
3170
|
+
/**
|
|
3171
|
+
* Does wrapped content of `w` × `h` fit sheet `p`? The title strip is
|
|
3172
|
+
* reserved across the whole width by default; content taller than that
|
|
3173
|
+
* may still fit when the group boxes that reach into the strip stay clear
|
|
3174
|
+
* of the title block's own corner (a person lays the left-hand column down
|
|
3175
|
+
* to the frame and keeps the bottom-right for the block).
|
|
3176
|
+
*/
|
|
3177
|
+
const fitsFrame = (p, w, h, rects) => {
|
|
3178
|
+
if (w > usableW(p))
|
|
3179
|
+
return { ok: false, intoStrip: false };
|
|
3180
|
+
if (h <= usableH(p))
|
|
3181
|
+
return { ok: true, intoStrip: false };
|
|
3182
|
+
if (h > usableH(p) + TITLE_STRIP - 4 * U)
|
|
3183
|
+
return { ok: false, intoStrip: false };
|
|
3184
|
+
// final placement: centred in width, top-aligned (the frame plus four units) in height
|
|
3185
|
+
const minX = Math.min(...rects.map((r) => r.x1));
|
|
3186
|
+
const minY = Math.min(...rects.map((r) => r.y1));
|
|
3187
|
+
const x0 = FRAME + Math.max(0, (usableW(p) - w) / 2) - minX;
|
|
3188
|
+
const y0 = FRAME + 4 * U - minY;
|
|
3189
|
+
const corner = { minX: p.w - FRAME - TITLE_BLOCK_W, minY: p.h - FRAME - TITLE_STRIP, maxX: p.w - FRAME, maxY: p.h - FRAME };
|
|
3190
|
+
const clear = rects.every((r) => !(r.x1 + x0 < corner.maxX && r.x2 + x0 > corner.minX && r.y1 + y0 < corner.maxY && r.y2 + y0 > corner.minY));
|
|
3191
|
+
return { ok: clear, intoStrip: clear };
|
|
3192
|
+
};
|
|
1191
3193
|
/**
|
|
1192
3194
|
* Whether the current placement fits sheet `p`, with the group shelf-wrap
|
|
1193
3195
|
* deltas that make it fit. `wrap` is null when there is nothing to reflow:
|
|
@@ -1196,8 +3198,29 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1196
3198
|
*/
|
|
1197
3199
|
const fitsOn = (p) => {
|
|
1198
3200
|
if (groupRects.length > 1) {
|
|
3201
|
+
const wrappedRects = (d) => groupRects.map((r, i) => ({ x1: r.x1 + d[i].dx, y1: r.y1 + d[i].dy, x2: r.x2 + d[i].dx, y2: r.y2 + d[i].dy }));
|
|
1199
3202
|
const w = wrapTo(usableW(p));
|
|
1200
|
-
|
|
3203
|
+
const fw = fitsFrame(p, w.w, w.h, wrappedRects(w.deltas));
|
|
3204
|
+
if (fw.ok)
|
|
3205
|
+
return { paper: p, wrap: { ...w, kind: 'rows' }, intoStrip: fw.intoStrip };
|
|
3206
|
+
// rows of mismatched heights may miss where columns of like widths fit
|
|
3207
|
+
for (const budgetH of [usableH(p), usableH(p) + TITLE_STRIP - 4 * U]) {
|
|
3208
|
+
const c = wrapColumns(budgetH);
|
|
3209
|
+
const fc = fitsFrame(p, c.w, c.h, wrappedRects(c.deltas));
|
|
3210
|
+
if (fc.ok)
|
|
3211
|
+
return { paper: p, wrap: c, intoStrip: fc.intoStrip };
|
|
3212
|
+
}
|
|
3213
|
+
for (const budgetH of [usableH(p), usableH(p) + TITLE_STRIP - 4 * U]) {
|
|
3214
|
+
for (let k = 2; k <= Math.min(4, groupRects.length - 1); k++) {
|
|
3215
|
+
const m = wrapMasonry(k, budgetH);
|
|
3216
|
+
if (!m)
|
|
3217
|
+
continue;
|
|
3218
|
+
const fm = fitsFrame(p, m.w, m.h, wrappedRects(m.deltas));
|
|
3219
|
+
if (fm.ok)
|
|
3220
|
+
return { paper: p, wrap: m, intoStrip: fm.intoStrip };
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
return null;
|
|
1201
3224
|
}
|
|
1202
3225
|
const r = groupRects[0];
|
|
1203
3226
|
return !r || (r.x2 - r.x1 <= usableW(p) && r.y2 - r.y1 <= usableH(p)) ? { paper: p, wrap: null } : null;
|
|
@@ -1219,15 +3242,36 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1219
3242
|
* spread the same cells into more side-by-side columns, so walk the height
|
|
1220
3243
|
* fractions until the content matches the sheet's aspect or nothing fits.
|
|
1221
3244
|
*/
|
|
3245
|
+
/** Closest miss per sheet a budgeted attempt could not fit, for the notes. */
|
|
3246
|
+
const budgetMisses = [];
|
|
1222
3247
|
const tryPaperBudgeted = (p) => {
|
|
1223
|
-
|
|
1224
|
-
|
|
3248
|
+
let best = null;
|
|
3249
|
+
// finer fractions re-row a tall group's columns until the groups are
|
|
3250
|
+
// short enough for two or three rows of them to stack on the sheet
|
|
3251
|
+
for (const [frac, div] of [1, 0.7, 0.5, 0.4, 0.35, 0.3, 0.25].flatMap((fr) => [1, 2, 2.5, 3].map((d) => [fr, d]))) {
|
|
3252
|
+
// band budgets of a half and a third of the sheet fold a wide flat
|
|
3253
|
+
// group (five button blocks in a row) toward the width of a column
|
|
3254
|
+
const b = placeAllGroups(Math.floor(usableW(p) / div / U), Math.floor((usableH(p) * frac) / U));
|
|
1225
3255
|
const f = fitsOn(p);
|
|
3256
|
+
if (groupRects.length > 1) {
|
|
3257
|
+
const w = wrapTo(usableW(p));
|
|
3258
|
+
trace(`try ${p.name} frac ${frac} div ${div}: wrapped ${w.w.toFixed(0)}x${w.h.toFixed(0)} vs ${usableW(p)}x${usableH(p)}; ${groupRects.map((r) => `${r.name.slice(0, 12)}=${(r.x2 - r.x1).toFixed(0)}x${(r.y2 - r.y1).toFixed(0)}`).join(' ')}`);
|
|
3259
|
+
}
|
|
1226
3260
|
if (f)
|
|
1227
3261
|
return { fit: f, bands: b };
|
|
3262
|
+
// remember the closest miss so a failed pass can say how far off it was
|
|
3263
|
+
if (groupRects.length) {
|
|
3264
|
+
const w = groupRects.length > 1 ? wrapTo(usableW(p)) : { w: groupRects[0].x2 - groupRects[0].x1, h: groupRects[0].y2 - groupRects[0].y1 };
|
|
3265
|
+
const over = Math.max(w.w - usableW(p), w.h - usableH(p));
|
|
3266
|
+
if (!best || over < Math.max(best.w - usableW(p), best.h - usableH(p)))
|
|
3267
|
+
best = { w: w.w, h: w.h };
|
|
3268
|
+
}
|
|
1228
3269
|
}
|
|
3270
|
+
if (best)
|
|
3271
|
+
budgetMisses.push(`${p.name} ${Math.round(best.w)}×${Math.round(best.h)} mm vs ${usableW(p)}×${usableH(p)} usable`);
|
|
1229
3272
|
return null;
|
|
1230
3273
|
};
|
|
3274
|
+
let compaction = hinted ? 'pinned' : 'not-needed';
|
|
1231
3275
|
let bands = placeAllGroups(Infinity);
|
|
1232
3276
|
let fit = bestFit();
|
|
1233
3277
|
if (!fit) {
|
|
@@ -1244,10 +3288,12 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1244
3288
|
if (t) {
|
|
1245
3289
|
bands = t.bands;
|
|
1246
3290
|
fit = t.fit;
|
|
3291
|
+
compaction = 'banded';
|
|
1247
3292
|
break;
|
|
1248
3293
|
}
|
|
1249
3294
|
}
|
|
1250
3295
|
if (!fit) {
|
|
3296
|
+
compaction = 'overflow';
|
|
1251
3297
|
const largest = candidates[candidates.length - 1];
|
|
1252
3298
|
bands = placeAllGroups(Math.floor(usableW(largest) / U), Math.floor(usableH(largest) / U));
|
|
1253
3299
|
fit = { paper: largest, wrap: groupRects.length > 1 ? wrapTo(usableW(largest)) : null };
|
|
@@ -1288,6 +3334,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1288
3334
|
}
|
|
1289
3335
|
}
|
|
1290
3336
|
if (compacted) {
|
|
3337
|
+
compaction = 'compacted';
|
|
1291
3338
|
bands = compactedBands;
|
|
1292
3339
|
fit = compacted;
|
|
1293
3340
|
notes.push(`sheet compacted: the natural layout fit ${naturalPaper.name} at ${Math.round(naturalUtil * 100)}% utilization; reflowed onto ${fit.paper.name}`);
|
|
@@ -1297,8 +3344,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1297
3344
|
}
|
|
1298
3345
|
}
|
|
1299
3346
|
else {
|
|
1300
|
-
// nothing smaller holds the reflowed content:
|
|
1301
|
-
// placement byte for byte
|
|
3347
|
+
// nothing smaller holds the reflowed content: say so, with how far
|
|
3348
|
+
// each sheet missed, then restore the natural placement byte for byte
|
|
3349
|
+
compaction = 'failed';
|
|
3350
|
+
notes.push(`sheet not compacted: the natural layout fits ${naturalPaper.name} at ${Math.round(naturalUtil * 100)}% utilization, but no smaller sheet holds the reflowed content (${budgetMisses.join('; ')})`);
|
|
1302
3351
|
bands = placeAllGroups(Infinity);
|
|
1303
3352
|
fit = bestFit();
|
|
1304
3353
|
}
|
|
@@ -1306,9 +3355,21 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1306
3355
|
}
|
|
1307
3356
|
if (fit.wrap) {
|
|
1308
3357
|
const wrap = fit.wrap;
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
notes.push(`groups wrapped onto ${
|
|
3358
|
+
if (wrap.kind === 'columns') {
|
|
3359
|
+
const cols = new Set(wrap.deltas.map((d, i) => Math.round((groupRects[i].x1 + d.dx) / U))).size;
|
|
3360
|
+
notes.push(`groups wrapped onto ${cols} columns to fit the sheet (declared order runs down each column)`);
|
|
3361
|
+
}
|
|
3362
|
+
else if (wrap.kind === 'masonry') {
|
|
3363
|
+
const cols = new Set(wrap.deltas.map((d, i) => Math.round((groupRects[i].x1 + d.dx) / U))).size;
|
|
3364
|
+
notes.push(`groups laid in ${cols} columns to fit the sheet (declared order runs along the rows; each column stacks its own groups)`);
|
|
3365
|
+
}
|
|
3366
|
+
else {
|
|
3367
|
+
// rows, like columns above, are counted by where their boxes land, not
|
|
3368
|
+
// by the shift each took to get there (every group starts elsewhere)
|
|
3369
|
+
const rows = new Set(wrap.deltas.map((d, i) => Math.round((groupRects[i].y1 + d.dy) / U))).size;
|
|
3370
|
+
if (rows > 1)
|
|
3371
|
+
notes.push(`groups wrapped onto ${rows} rows to fit the sheet`);
|
|
3372
|
+
}
|
|
1312
3373
|
groupRects.forEach((r, i) => {
|
|
1313
3374
|
const d = wrap.deltas[i];
|
|
1314
3375
|
if (!d.dx && !d.dy)
|
|
@@ -1555,7 +3616,8 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1555
3616
|
const joinClear = (prev, c) => {
|
|
1556
3617
|
const y = c.ep.at.y + c.o.dy * STUB * U;
|
|
1557
3618
|
const seg = { x1: prev.ep.at.x, y1: y, x2: c.ep.at.x, y2: y };
|
|
1558
|
-
return (
|
|
3619
|
+
return (groupOf.get(prev.ep.ref) === groupOf.get(c.ep.ref) &&
|
|
3620
|
+
c.ep.at.x - prev.ep.at.x <= BANK_PITCH_MAX * U &&
|
|
1559
3621
|
!powerBodies.some((b) => segCrossesBody(seg.x1, seg.y1, seg.x2, seg.y2, b)) &&
|
|
1560
3622
|
!touchesForeign([seg], net.name, ownEps, { predictStubs: true }) &&
|
|
1561
3623
|
!crossesForeignStubLine(seg));
|
|
@@ -1595,10 +3657,15 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1595
3657
|
if (len > maxLen)
|
|
1596
3658
|
len = maxLen;
|
|
1597
3659
|
const at = (l) => ({ x: ep.at.x + o.dx * l * U, y: ep.at.y + o.dy * l * U });
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
3660
|
+
// A vertical stub carries its name beyond the bar, in line. A HORIZONTAL
|
|
3661
|
+
// stub used to put the name 3.556 mm above (rails) or below (grounds)
|
|
3662
|
+
// the bar: 1.4 pin rows off, so on any IC with pins one row apart the
|
|
3663
|
+
// rail name of pin 5 sat on the label of pin 4 (esp32-amp's eFuse,
|
|
3664
|
+
// VBUS_FUSED over EFUSE_USB_FLT). The name now continues outward along
|
|
3665
|
+
// the row, past the bar, where the row is its own.
|
|
3666
|
+
const valueAtOf = (end) => o.dy !== 0
|
|
3667
|
+
? { x: end.x, y: end.y + o.dy * 3.556 }
|
|
3668
|
+
: { x: end.x + o.dx * (Math.max(1, net.name.length) * LABEL_ADVANCE * LABEL_HEIGHT / 2 + 1.905), y: end.y };
|
|
1602
3669
|
// Adjacent power pins collide their value texts two ways, resolved two
|
|
1603
3670
|
// ways. The SAME net repeated (a TQFP's VCC pins one row apart) hides
|
|
1604
3671
|
// the duplicates: one visible name per cluster carries the same
|
|
@@ -1762,27 +3829,25 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1762
3829
|
const o = outward(ep.pin);
|
|
1763
3830
|
return { ep, end: { x: ep.at.x + o.dx * STUB * U, y: ep.at.y + o.dy * STUB * U }, o };
|
|
1764
3831
|
});
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
//
|
|
1775
|
-
|
|
1776
|
-
const ys = stubs.map((s) => s.end.y);
|
|
3832
|
+
const routeStubs = (subset) => {
|
|
3833
|
+
const xs = subset.map((s) => s.end.x).sort((a, b) => a - b);
|
|
3834
|
+
const ys = subset.map((s) => s.end.y);
|
|
3835
|
+
const admit = (candidate) => {
|
|
3836
|
+
if (candidate.some((c) => bodies.some((b) => segCrossesBody(c.x1, c.y1, c.x2, c.y2, b))))
|
|
3837
|
+
return false;
|
|
3838
|
+
return !touchesForeign(candidate, net.name, new Set(net.pins));
|
|
3839
|
+
};
|
|
3840
|
+
const found = [];
|
|
3841
|
+
// A vertical trunk with horizontal branches, at the median stub x,
|
|
3842
|
+
// right of everything, left of everything.
|
|
1777
3843
|
const trunkCandidates = [
|
|
1778
3844
|
grid(Math.round(xs[Math.floor(xs.length / 2)] / U)),
|
|
1779
3845
|
grid(Math.round(xs[xs.length - 1] / U) + STUB),
|
|
1780
3846
|
grid(Math.round(xs[0] / U) - STUB),
|
|
1781
3847
|
];
|
|
1782
|
-
let routed = false;
|
|
1783
3848
|
for (const trunkX of trunkCandidates) {
|
|
1784
3849
|
const candidate = [];
|
|
1785
|
-
for (const s of
|
|
3850
|
+
for (const s of subset) {
|
|
1786
3851
|
candidate.push({ x1: s.ep.at.x, y1: s.ep.at.y, x2: s.end.x, y2: s.end.y });
|
|
1787
3852
|
if (!sameCoord(s.end.x, trunkX))
|
|
1788
3853
|
candidate.push({ x1: s.end.x, y1: s.end.y, x2: trunkX, y2: s.end.y });
|
|
@@ -1793,74 +3858,269 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1793
3858
|
for (let i = 1; i < meetYs.length; i++) {
|
|
1794
3859
|
candidate.push({ x1: trunkX, y1: meetYs[i - 1], x2: trunkX, y2: meetYs[i] });
|
|
1795
3860
|
}
|
|
1796
|
-
if (candidate
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
3861
|
+
if (admit(candidate))
|
|
3862
|
+
found.push({ segs: candidate, trunkX });
|
|
3863
|
+
}
|
|
3864
|
+
// A comb: the trunk lies ALONG one endpoint's row — the IC pin's row
|
|
3865
|
+
// when the net has one — and every other stub rises or drops to it.
|
|
3866
|
+
// This is how a pin with parts hung on it is drawn: the row runs out to
|
|
3867
|
+
// the axis, the chain above and the chain below meet it in one T. The
|
|
3868
|
+
// vertical trunk alone could not route a pull-up and a pull-down on one
|
|
3869
|
+
// pin (its branch to the second part ran along a foreign row), so one
|
|
3870
|
+
// of them shipped labelled.
|
|
3871
|
+
const rows = [...new Map(subset.map((s) => [knum(s.end.y), s])).values()].sort((a, b) => {
|
|
3872
|
+
const ica = (placed.get(a.ep.ref)?.sym.pins.length ?? 0) >= 3 ? 0 : 1;
|
|
3873
|
+
const icb = (placed.get(b.ep.ref)?.sym.pins.length ?? 0) >= 3 ? 0 : 1;
|
|
3874
|
+
return ica - icb || a.end.y - b.end.y;
|
|
3875
|
+
});
|
|
3876
|
+
for (const rowStub of rows) {
|
|
3877
|
+
const trunkY = rowStub.end.y;
|
|
3878
|
+
const candidate = [];
|
|
3879
|
+
for (const s of subset) {
|
|
3880
|
+
candidate.push({ x1: s.ep.at.x, y1: s.ep.at.y, x2: s.end.x, y2: s.end.y });
|
|
3881
|
+
if (!sameCoord(s.end.y, trunkY))
|
|
3882
|
+
candidate.push({ x1: s.end.x, y1: s.end.y, x2: s.end.x, y2: trunkY });
|
|
3883
|
+
}
|
|
3884
|
+
const meetXs = [...new Map(xs.map((x) => [knum(x), x])).values()].sort((a, b) => a - b);
|
|
3885
|
+
for (let i = 1; i < meetXs.length; i++) {
|
|
3886
|
+
candidate.push({ x1: meetXs[i - 1], y1: trunkY, x2: meetXs[i], y2: trunkY });
|
|
3887
|
+
}
|
|
3888
|
+
if (admit(candidate))
|
|
3889
|
+
found.push({ segs: candidate, trunkX: meetXs[0], trunkY });
|
|
3890
|
+
}
|
|
3891
|
+
// A hook, for two stubs where one lies on a row and the other is a
|
|
3892
|
+
// lead below or above it on an axis the row would have to cross: run
|
|
3893
|
+
// the row past the lead by two units, turn to the lead's level, and
|
|
3894
|
+
// come back to its stub end. This is how a bootstrap cap's far lead is
|
|
3895
|
+
// reached from the row it bridges to (the other lead sits on the same
|
|
3896
|
+
// axis, in the way of a straight drop).
|
|
3897
|
+
if (!found.length && subset.length === 2) {
|
|
3898
|
+
for (const [a, b] of [[subset[0], subset[1]], [subset[1], subset[0]]]) {
|
|
3899
|
+
if (a.o.dx === 0 || b.o.dy === 0)
|
|
3900
|
+
continue; // a: a sideways pin (the row); b: a vertical lead
|
|
3901
|
+
for (const past of [2 * U, -2 * U]) {
|
|
3902
|
+
const turnX = grid(Math.round((b.end.x + past) / U));
|
|
3903
|
+
const candidate = [
|
|
3904
|
+
{ x1: a.ep.at.x, y1: a.ep.at.y, x2: a.end.x, y2: a.end.y },
|
|
3905
|
+
{ x1: a.end.x, y1: a.end.y, x2: turnX, y2: a.end.y },
|
|
3906
|
+
{ x1: turnX, y1: a.end.y, x2: turnX, y2: b.end.y },
|
|
3907
|
+
{ x1: turnX, y1: b.end.y, x2: b.end.x, y2: b.end.y },
|
|
3908
|
+
{ x1: b.ep.at.x, y1: b.ep.at.y, x2: b.end.x, y2: b.end.y },
|
|
3909
|
+
].filter((c) => !(sameCoord(c.x1, c.x2) && sameCoord(c.y1, c.y2)));
|
|
3910
|
+
if (admit(candidate)) {
|
|
3911
|
+
found.push({ segs: candidate, trunkX: turnX });
|
|
3912
|
+
break;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
if (found.length)
|
|
3916
|
+
break;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
if (!found.length) {
|
|
3920
|
+
trace(`route ${net.name}: no clean route for ${subset.map((s) => `${s.ep.ref}.${s.ep.pin.number}`).join(', ')}`);
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
// the fewest segments is the fewest bends; ties keep the classic order
|
|
3924
|
+
return found.reduce((best, r) => (r.segs.length < best.segs.length ? r : best), found[0]);
|
|
3925
|
+
};
|
|
3926
|
+
/** Labels naming this net's wired runs, promoted to flags below if the net also leaves through a stub. */
|
|
3927
|
+
const netWired = [];
|
|
3928
|
+
/** Draw a routed subset: its wires, one label naming the net, and junction dots. */
|
|
3929
|
+
const commitRoute = (subset, r) => {
|
|
3930
|
+
for (const c of r.segs)
|
|
3931
|
+
addWire(net.name, c.x1, c.y1, c.x2, c.y2);
|
|
3932
|
+
// one label names the wired run (topmost-leftmost wire point): the net
|
|
3933
|
+
// stays identifiable to PINOUT/drift and to a reviewer without a
|
|
3934
|
+
// label-per-pin, matching hand-drafting practice
|
|
3935
|
+
const pts = r.segs.flatMap((c) => [
|
|
3936
|
+
{ x: c.x1, y: c.y1 },
|
|
3937
|
+
{ x: c.x2, y: c.y2 },
|
|
3938
|
+
]);
|
|
3939
|
+
pts.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
3940
|
+
// The name stands above the wire from its anchor rightward. A trunk's
|
|
3941
|
+
// top is the classic place; a run that is one horizontal row (a part
|
|
3942
|
+
// on its pin's row) has no trunk, and its left end is the pin, so the
|
|
3943
|
+
// name sits over the stub and the gap the row left for it. Whichever
|
|
3944
|
+
// candidate clears every body wins; the run's top-left point is the
|
|
3945
|
+
// last resort.
|
|
3946
|
+
const vertTops = r.segs.filter((c) => sameCoord(c.x1, c.x2)).map((c) => ({ x: c.x1, y: Math.min(c.y1, c.y2) }));
|
|
3947
|
+
const candidates = [...vertTops.sort((a, b) => a.y - b.y || a.x - b.x), pts[0]];
|
|
3948
|
+
const clearOfBodies = (pt) => {
|
|
3949
|
+
const b = labelTextBox(net.name, pt.x, pt.y, 0);
|
|
3950
|
+
if ([...placed.values()].some((pl) => boundsOverlap(b, pl.body)))
|
|
3951
|
+
return false;
|
|
3952
|
+
return !labels.some((o) => o.name !== net.name && boundsOverlap(padBox(b), labelTextBox(o.name, o.x, o.y, o.rot)));
|
|
3953
|
+
};
|
|
3954
|
+
const at = candidates.find(clearOfBodies) ?? pts[0];
|
|
3955
|
+
labels.push({ name: net.name, x: at.x, y: at.y, rot: 0, kind: 'local' });
|
|
3956
|
+
netWired.push(labels.length - 1);
|
|
3957
|
+
wiredLabels.push({ label: labels.length - 1, pts, segs: r.segs.map((c) => ({ ...c })) });
|
|
3958
|
+
wired++;
|
|
3959
|
+
if (subset.length > 2) {
|
|
3960
|
+
if (r.trunkY !== undefined) {
|
|
3961
|
+
// comb: a branch meeting the row trunk strictly inside its span is a T
|
|
3962
|
+
const xs = subset.map((s) => s.end.x);
|
|
3963
|
+
for (const s of subset) {
|
|
3964
|
+
const meet = sameCoord(s.end.y, r.trunkY) ? s.end : { x: s.end.x, y: r.trunkY };
|
|
3965
|
+
if (meet.x > Math.min(...xs) + 0.01 && meet.x < Math.max(...xs) - 0.01)
|
|
3966
|
+
junctions.push(meet);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
else {
|
|
3970
|
+
const ys = subset.map((s) => s.end.y);
|
|
3971
|
+
for (const s of subset) {
|
|
3972
|
+
const meet = sameCoord(s.end.x, r.trunkX) ? s.end : { x: r.trunkX, y: s.end.y };
|
|
1820
3973
|
if (meet.y > Math.min(...ys) && meet.y < Math.max(...ys))
|
|
1821
3974
|
junctions.push(meet);
|
|
1822
3975
|
}
|
|
1823
3976
|
}
|
|
1824
|
-
routed = true;
|
|
1825
|
-
break;
|
|
1826
3977
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
3978
|
+
};
|
|
3979
|
+
// Clusters: endpoints in one group whose stub ends sit within wire span of
|
|
3980
|
+
// one another, in x order. A net that stays in one group and fits the
|
|
3981
|
+
// span is one cluster — the classic case, drawn as before. A net that
|
|
3982
|
+
// also leaves the group, or runs longer, still gets each local run wired
|
|
3983
|
+
// and labelled once: adjacency before labelling (#220 phase 3), so a part
|
|
3984
|
+
// hung on a pin or lying on its row is wired to it whatever the rest of
|
|
3985
|
+
// the net does. Lone endpoints keep a stub and a label.
|
|
3986
|
+
const sorted = [...stubs].sort((a, b) => a.end.x - b.end.x || a.end.y - b.end.y);
|
|
3987
|
+
const clusters = [];
|
|
3988
|
+
const anchorKey = (ref) => {
|
|
3989
|
+
let k = ref;
|
|
3990
|
+
for (let i = 0; i < 4 && boundTo.has(k); i++)
|
|
3991
|
+
k = boundTo.get(k);
|
|
3992
|
+
return k;
|
|
3993
|
+
};
|
|
3994
|
+
const near = (a, b) => (Math.abs(a.end.x - b.end.x) <= MAX_WIRE_SPAN && Math.abs(a.end.y - b.end.y) <= MAX_WIRE_SPAN) ||
|
|
3995
|
+
// a part hung on a pin or lying on its row joins that pin's cluster
|
|
3996
|
+
// however far its lane sits: the fourth lane of a comb is past the
|
|
3997
|
+
// wire span, and it is still that pin's part
|
|
3998
|
+
anchorKey(a.ep.ref) === anchorKey(b.ep.ref);
|
|
3999
|
+
for (const s of sorted) {
|
|
4000
|
+
const home = clusters.find((cl) => groupOf.get(cl[0].ep.ref) === groupOf.get(s.ep.ref) && cl.every((o) => near(o, s)));
|
|
4001
|
+
if (home)
|
|
4002
|
+
home.push(s);
|
|
4003
|
+
else
|
|
4004
|
+
clusters.push([s]);
|
|
4005
|
+
}
|
|
4006
|
+
const wiredStubs = new Set();
|
|
4007
|
+
for (const found of clusters) {
|
|
4008
|
+
// A cluster past the wired-net size is a pin with its hung parts plus
|
|
4009
|
+
// whatever else sits within span: it is narrowed to the largest set of
|
|
4010
|
+
// endpoints bound to one anchor, so the parts hung on a pin are wired
|
|
4011
|
+
// to it however many there are, and the rest keep stubs and labels.
|
|
4012
|
+
let cl = found;
|
|
4013
|
+
if (cl.length > MAX_WIRED_ENDPOINTS) {
|
|
4014
|
+
const byAnchor = new Map();
|
|
4015
|
+
for (const s of cl)
|
|
4016
|
+
byAnchor.set(anchorKey(s.ep.ref), [...(byAnchor.get(anchorKey(s.ep.ref)) ?? []), s]);
|
|
4017
|
+
cl = [...byAnchor.values()].reduce((a, b) => (b.length > a.length ? b : a));
|
|
4018
|
+
trace(`route ${net.name}: cluster of ${found.length} narrowed to ${cl.length} bound to ${anchorKey(cl[0].ep.ref)}`);
|
|
4019
|
+
}
|
|
4020
|
+
if (cl.length < 2)
|
|
4021
|
+
continue;
|
|
4022
|
+
// the whole cluster, else the largest subset that routes: a run on the
|
|
4023
|
+
// IC pin's row still draws as a wire when a third endpoint's branch
|
|
4024
|
+
// cannot be cleared, and only that endpoint keeps a stub label. Subsets
|
|
4025
|
+
// are tried largest first in index order, at most ROUTE_ATTEMPTS.
|
|
4026
|
+
let attempts = 0;
|
|
4027
|
+
let done = false;
|
|
4028
|
+
const cur = [];
|
|
4029
|
+
const pick = (start, size) => {
|
|
4030
|
+
if (done || attempts >= ROUTE_ATTEMPTS)
|
|
4031
|
+
return;
|
|
4032
|
+
if (cur.length === size) {
|
|
4033
|
+
attempts++;
|
|
4034
|
+
const r = routeStubs(cur);
|
|
4035
|
+
if (!r)
|
|
4036
|
+
return;
|
|
4037
|
+
const sub = [...cur];
|
|
4038
|
+
commitRoute(sub, r);
|
|
4039
|
+
for (const s of sub)
|
|
4040
|
+
wiredStubs.add(s);
|
|
4041
|
+
done = true;
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
for (let i = start; i <= cl.length - (size - cur.length) && !done; i++) {
|
|
4045
|
+
cur.push(cl[i]);
|
|
4046
|
+
pick(i + 1, size);
|
|
4047
|
+
cur.pop();
|
|
4048
|
+
}
|
|
4049
|
+
};
|
|
4050
|
+
for (let size = cl.length; size >= 2 && !done && attempts < ROUTE_ATTEMPTS; size--)
|
|
4051
|
+
pick(0, size);
|
|
4052
|
+
if (!done && attempts >= ROUTE_ATTEMPTS)
|
|
4053
|
+
trace(`route ${net.name}: ${ROUTE_ATTEMPTS} subsets of ${cl.length} tried, none routed`);
|
|
4054
|
+
}
|
|
4055
|
+
for (const s of stubs) {
|
|
4056
|
+
if (wiredStubs.has(s))
|
|
4057
|
+
continue;
|
|
4058
|
+
// A stub is a wire too: its endpoint resting on a foreign net's wire
|
|
4059
|
+
// or connection point merges nets exactly like a trunk would (the
|
|
4060
|
+
// cap-to-ground drop's 2-unit stub ended on the neighbouring power
|
|
4061
|
+
// pin's stub interior — I22's third face). Grow the stub a grid unit
|
|
4062
|
+
// at a time until the endpoint is clear; the interior then CROSSES
|
|
4063
|
+
// the foreign wire mid-segment, which does not connect. If no length
|
|
4064
|
+
// clears, emit the plain stub and let the merged-net gate refuse
|
|
4065
|
+
// loudly rather than ship the contact.
|
|
4066
|
+
let end = s.end;
|
|
4067
|
+
const own = new Set(net.pins);
|
|
4068
|
+
// Rungs in preference order: the classic 0..2 extensions first so
|
|
4069
|
+
// clear cases stay byte-identical, then deeper extensions, then a
|
|
4070
|
+
// one-unit retreat. A stub that ships with NO clear rung still ends
|
|
4071
|
+
// touching a foreign wire and the merge gate refuses the draft, so
|
|
4072
|
+
// every extra rung here is a board that drafts instead of refusing
|
|
4073
|
+
// (jetson's twelve wire-contact refusals were exactly this fallback).
|
|
4074
|
+
for (const len of [STUB, STUB + 1, STUB + 2, STUB + 3, STUB + 4, STUB + 5, STUB + 6, 1]) {
|
|
4075
|
+
const cand = {
|
|
4076
|
+
x: s.ep.at.x + s.o.dx * len * U,
|
|
4077
|
+
y: s.ep.at.y + s.o.dy * len * U,
|
|
4078
|
+
};
|
|
4079
|
+
if (!touchesForeign([{ x1: s.ep.at.x, y1: s.ep.at.y, x2: cand.x, y2: cand.y }], net.name, own)) {
|
|
4080
|
+
end = cand;
|
|
4081
|
+
break;
|
|
1857
4082
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
4083
|
+
}
|
|
4084
|
+
addWire(net.name, s.ep.at.x, s.ep.at.y, end.x, end.y);
|
|
4085
|
+
// a stub ends in a global flag that continues the stub's direction,
|
|
4086
|
+
// the way a person hangs a flag on a wire end: a leftward stub reads
|
|
4087
|
+
// leftward, an upward one reads upward; the shape says what the pin is
|
|
4088
|
+
labels.push({ name: net.name, x: end.x, y: end.y, rot: rotOutward(s.o), kind: 'global', shape: flagShape(s.ep.pin.etype) });
|
|
4089
|
+
stubbedLabels.push({ label: labels.length - 1, wire: wires.length - 1, o: s.o, pins: net.pins });
|
|
4090
|
+
labelled++;
|
|
4091
|
+
}
|
|
4092
|
+
// A net that also leaves its run through a stub is named by global flags
|
|
4093
|
+
// there, and a local label of the same name would not connect to them in
|
|
4094
|
+
// KiCad: the run's own name becomes a flag too, re-anchored where a flag
|
|
4095
|
+
// fits (a local name stands over its wire; a flag has a body of its own).
|
|
4096
|
+
// A net wired whole keeps its plain local name on the run.
|
|
4097
|
+
if (netWired.length && stubs.some((s) => !wiredStubs.has(s))) {
|
|
4098
|
+
for (const idx of netWired) {
|
|
4099
|
+
const lb = labels[idx];
|
|
4100
|
+
const rec = wiredLabels.find((w) => w.label === idx);
|
|
4101
|
+
lb.kind = 'global';
|
|
4102
|
+
lb.shape = 'passive';
|
|
4103
|
+
const clear = (c) => ![...placed.values()].some((pl) => boundsOverlap(labelTextBox(lb.name, c.x, c.y, c.rot, 'global'), pl.body));
|
|
4104
|
+
const at = wiredFlagCandidates(rec.segs).find(clear) ?? { x: lb.x, y: lb.y, rot: 90 };
|
|
4105
|
+
trace(`label ${lb.name}: run name becomes a flag at (${at.x}, ${at.y}, ${at.rot})`);
|
|
4106
|
+
lb.x = at.x;
|
|
4107
|
+
lb.y = at.y;
|
|
4108
|
+
lb.rot = at.rot;
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
if (process.env['COPPERHEAD_DRAFT_TRACE'] === '1') {
|
|
4113
|
+
// every crossing of two nets' wires, for the trace: a comb whose lanes
|
|
4114
|
+
// cross another pin's row is legal, but a drafter wants to know where
|
|
4115
|
+
const H = wires.filter((w) => sameCoord(w.y1, w.y2));
|
|
4116
|
+
const V = wires.filter((w) => sameCoord(w.x1, w.x2));
|
|
4117
|
+
for (const h of H) {
|
|
4118
|
+
for (const v of V) {
|
|
4119
|
+
if (h.net === v.net)
|
|
4120
|
+
continue;
|
|
4121
|
+
const hx1 = Math.min(h.x1, h.x2), hx2 = Math.max(h.x1, h.x2), vy1 = Math.min(v.y1, v.y2), vy2 = Math.max(v.y1, v.y2);
|
|
4122
|
+
if (v.x1 > hx1 + 0.01 && v.x1 < hx2 - 0.01 && h.y1 > vy1 + 0.01 && h.y1 < vy2 - 0.01)
|
|
4123
|
+
trace(`crossing ${h.net} (row y=${h.y1}) x ${v.net} (x=${v.x1})`);
|
|
1864
4124
|
}
|
|
1865
4125
|
}
|
|
1866
4126
|
}
|
|
@@ -1876,11 +4136,39 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1876
4136
|
/** What KiCad renders for the reference: a multi-unit instance shows its
|
|
1877
4137
|
* unit letter (U1A, U1B), so width metrics must measure the rendered text. */
|
|
1878
4138
|
const displayRefOf = (pl) => pl.unit !== null ? `${pl.refDes}${String.fromCharCode(64 + Math.min(pl.unit, 26))}` : pl.refDes;
|
|
4139
|
+
/**
|
|
4140
|
+
* Width of the body interior left free by the pin NAMES drawn inside it on
|
|
4141
|
+
* the rows a ref/value pair at `cy` would occupy. A large module's names
|
|
4142
|
+
* (ESP32-WROVER's "MTMS/GPIO14/ADC2_CH6") run most of the way across, and a
|
|
4143
|
+
* value dropped on the centre line read as one word with them.
|
|
4144
|
+
*/
|
|
4145
|
+
const interiorSpan = (pl, cy) => {
|
|
4146
|
+
let leftW = 0;
|
|
4147
|
+
let rightW = 0;
|
|
4148
|
+
for (const pin of pl.sym.pins) {
|
|
4149
|
+
const o = outward(pin);
|
|
4150
|
+
if (o.dx === 0)
|
|
4151
|
+
continue;
|
|
4152
|
+
const py = pinAt(pl, pin).y;
|
|
4153
|
+
if (Math.abs(py - cy) > 2.54 + 0.7)
|
|
4154
|
+
continue;
|
|
4155
|
+
const w = Math.max(0, pin.name.length) * NAME_ADVANCE * LABEL_HEIGHT;
|
|
4156
|
+
if (o.dx === -1)
|
|
4157
|
+
leftW = Math.max(leftW, w);
|
|
4158
|
+
else
|
|
4159
|
+
rightW = Math.max(rightW, w);
|
|
4160
|
+
}
|
|
4161
|
+
return { left: pl.body.minX + leftW, right: pl.body.maxX - rightW };
|
|
4162
|
+
};
|
|
4163
|
+
/** A library symbol that draws text of its own inside the body (ESP32-WROVER
|
|
4164
|
+
* prints its family name across the middle) has no free interior. */
|
|
4165
|
+
const hasGraphicText = (pl) => /\(text\s/.test(pl.sym.sourceText ?? '');
|
|
4166
|
+
const pinSidesOf = (pl) => new Set(pl.sym.pins.map((p) => {
|
|
4167
|
+
const o = outward(p);
|
|
4168
|
+
return o.dx === -1 ? 'left' : o.dx === 1 ? 'right' : o.dy === -1 ? 'top' : 'bottom';
|
|
4169
|
+
}));
|
|
1879
4170
|
for (const [, pl] of [...placed.entries()].sort((a, b) => a[0].localeCompare(b[0], undefined, { numeric: true }))) {
|
|
1880
|
-
const pinSides =
|
|
1881
|
-
const o = outward(p);
|
|
1882
|
-
return o.dx === -1 ? 'left' : o.dx === 1 ? 'right' : o.dy === -1 ? 'top' : 'bottom';
|
|
1883
|
-
}));
|
|
4171
|
+
const pinSides = pinSidesOf(pl);
|
|
1884
4172
|
const cy = (pl.body.minY + pl.body.maxY) / 2;
|
|
1885
4173
|
const cx = (pl.body.minX + pl.body.maxX) / 2;
|
|
1886
4174
|
const textW = Math.max(displayRefOf(pl).length, pl.part.value.length) * 0.8 * 1.27;
|
|
@@ -1892,14 +4180,21 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1892
4180
|
// below the body otherwise
|
|
1893
4181
|
valueAt = pinSides.has('bottom') ? { x: cx, y: pl.body.minY - 5.08 } : { x: cx, y: pl.body.maxY + 2.54 };
|
|
1894
4182
|
}
|
|
1895
|
-
else if (pl.body.
|
|
1896
|
-
pl
|
|
4183
|
+
else if (pl.body.maxY - pl.body.minY >= 7.62 &&
|
|
4184
|
+
!hasGraphicText(pl) &&
|
|
4185
|
+
// the name at its real advance, centred in the interval the pin names
|
|
4186
|
+
// leave free, with two text heights of air each side: measured from
|
|
4187
|
+
// the body centre instead, ESP32-WROVER's value printed across the
|
|
4188
|
+
// ends of its long left-hand pin names
|
|
4189
|
+
interiorSpan(pl, cy).right - interiorSpan(pl, cy).left >= (textW * NAME_ADVANCE) / LABEL_ADVANCE + 4 * 2.54) {
|
|
1897
4190
|
// pins on top AND a body big enough to hold its own name: a TQFP-class
|
|
1898
4191
|
// part carries pins on all four sides, so every outside slot lands on
|
|
1899
4192
|
// some pin's stub or label; the body interior is the one guaranteed-free
|
|
1900
4193
|
// area, and it is where KiCad's own large symbols put their text
|
|
1901
|
-
|
|
1902
|
-
|
|
4194
|
+
const span = interiorSpan(pl, cy);
|
|
4195
|
+
const mid = grid(Math.round((span.left + span.right) / 2 / U));
|
|
4196
|
+
refAt = { x: mid, y: cy - 1.27 };
|
|
4197
|
+
valueAt = { x: mid, y: cy + 1.27 };
|
|
1903
4198
|
}
|
|
1904
4199
|
else {
|
|
1905
4200
|
refAt = { x: pl.body.maxX + textW / 2 + 1.27, y: cy - 1.27 };
|
|
@@ -1910,7 +4205,8 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1910
4205
|
libId: pl.sym.libId,
|
|
1911
4206
|
value: pl.part.value,
|
|
1912
4207
|
footprint: pl.part.footprint ?? '',
|
|
1913
|
-
at: { x: pl.x, y: pl.y, rot:
|
|
4208
|
+
at: { x: pl.x, y: pl.y, rot: pl.rot },
|
|
4209
|
+
...(pl.mirror ? { mirror: pl.mirror } : {}),
|
|
1914
4210
|
refAt,
|
|
1915
4211
|
valueAt,
|
|
1916
4212
|
pinNumbers: pl.sym.pins.map((p) => p.number),
|
|
@@ -1944,6 +4240,8 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1944
4240
|
const fieldBoxes = extraSymbols
|
|
1945
4241
|
.filter((s) => !s.hideValue)
|
|
1946
4242
|
.map((s) => centeredTextBox(s.value, s.valueAt.x, s.valueAt.y));
|
|
4243
|
+
const powerBodies = extraSymbols.map((s) => ({ minX: s.at.x - 2 * U, minY: s.at.y - 2 * U, maxX: s.at.x + 2 * U, maxY: s.at.y + 2 * U }));
|
|
4244
|
+
const labelBoxes = labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot, l.kind));
|
|
1947
4245
|
for (const { sym, pl } of emitPairs) {
|
|
1948
4246
|
const dref = displayRefOf(pl);
|
|
1949
4247
|
const cx = (pl.body.minX + pl.body.maxX) / 2;
|
|
@@ -1957,15 +4255,54 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1957
4255
|
if (op !== pl && boundsOverlap(b, op.body))
|
|
1958
4256
|
return false;
|
|
1959
4257
|
}
|
|
1960
|
-
|
|
4258
|
+
// a rail bar or ground symbol is a drawn body too, not just its
|
|
4259
|
+
// value text (R23's reference printed through PVDD's bar)
|
|
4260
|
+
if (powerBodies.some((pb) => boundsOverlap(b, pb)))
|
|
4261
|
+
return false;
|
|
4262
|
+
// the part's own pins: the pin line and its number, from the pin
|
|
4263
|
+
// end to the body edge (U8's name printed over its no-connect pins,
|
|
4264
|
+
// which draw no wire for the wire check to see)
|
|
4265
|
+
for (const pin of pl.sym.pins) {
|
|
4266
|
+
const p = pinAt(pl, pin);
|
|
4267
|
+
const o = outward(pin);
|
|
4268
|
+
const band = o.dx !== 0
|
|
4269
|
+
? { minX: Math.min(p.x, o.dx === 1 ? pl.body.maxX : pl.body.minX), maxX: Math.max(p.x, o.dx === 1 ? pl.body.maxX : pl.body.minX), minY: p.y - 1.5, maxY: p.y + 1.5 }
|
|
4270
|
+
: { minX: p.x - 1.5, maxX: p.x + 1.5, minY: Math.min(p.y, o.dy === 1 ? pl.body.maxY : pl.body.minY), maxY: Math.max(p.y, o.dy === 1 ? pl.body.maxY : pl.body.minY) };
|
|
4271
|
+
if (boundsOverlap(b, band))
|
|
4272
|
+
return false;
|
|
4273
|
+
}
|
|
4274
|
+
if (fieldBoxes.some((t) => boundsOverlap(t, padBox(b))))
|
|
4275
|
+
return false;
|
|
4276
|
+
// net labels are on the sheet by now; a slot on top of one is no slot
|
|
4277
|
+
if (labelBoxes.some((t) => boundsOverlap(t, padBox(b))))
|
|
1961
4278
|
return false;
|
|
1962
4279
|
}
|
|
1963
4280
|
return true;
|
|
1964
4281
|
};
|
|
1965
4282
|
if (!pairClear(sym.refAt, sym.valueAt)) {
|
|
1966
4283
|
const ladder = [];
|
|
1967
|
-
|
|
1968
|
-
|
|
4284
|
+
// corner slots: a part with a stub on its top or bottom centre (a
|
|
4285
|
+
// module's rail pin) still has its top-left and top-right free
|
|
4286
|
+
const xl = pl.body.minX + textW / 2 + 1.27;
|
|
4287
|
+
const xr = pl.body.maxX - textW / 2 - 1.27;
|
|
4288
|
+
// A part lying on a row (leads left and right) has rows one pitch
|
|
4289
|
+
// above and below it; its texts fit BETWEEN the rows, half a pitch
|
|
4290
|
+
// out from the body, wherever the neighbouring row carries no wire
|
|
4291
|
+
// there. Reference above and value below first, then both on one
|
|
4292
|
+
// side, then side by side on one line.
|
|
4293
|
+
if (pinSidesOf(pl).has('left') && pinSidesOf(pl).has('right') && !pinSidesOf(pl).has('top')) {
|
|
4294
|
+
const hh = 1.27;
|
|
4295
|
+
// Both texts on ONE side first, and side by side on one line before
|
|
4296
|
+
// stacked: two parts on rows two pitches apart then keep their texts
|
|
4297
|
+
// in their own half of the gap (C27's value and L3's reference met
|
|
4298
|
+
// in the middle of the five millimetres between their rows).
|
|
4299
|
+
ladder.push([{ x: cx, y: pl.body.minY - hh - 2.54 }, { x: cx, y: pl.body.minY - hh }], [{ x: cx - textW / 2 - 0.635, y: pl.body.minY - hh }, { x: cx + textW / 2 + 0.635, y: pl.body.minY - hh }], [{ x: cx, y: pl.body.maxY + hh }, { x: cx, y: pl.body.maxY + hh + 2.54 }], [{ x: cx - textW / 2 - 0.635, y: pl.body.maxY + hh }, { x: cx + textW / 2 + 0.635, y: pl.body.maxY + hh }], [{ x: cx, y: pl.body.minY - hh }, { x: cx, y: pl.body.maxY + hh }]);
|
|
4300
|
+
}
|
|
4301
|
+
// rungs reach past a rail symbol and its name on a top or bottom
|
|
4302
|
+
// stub (about 6 mm out), so a module pinned on all four sides still
|
|
4303
|
+
// finds a slot above or below itself instead of on its own labels
|
|
4304
|
+
for (const extra of [0, 2.54, 5.08, 7.62, 10.16]) {
|
|
4305
|
+
ladder.push([{ x: cx, y: pl.body.maxY + 2.54 + extra }, { x: cx, y: pl.body.maxY + 5.08 + extra }], [{ x: cx, y: pl.body.minY - 5.08 - extra }, { x: cx, y: pl.body.minY - 2.54 - extra }], [{ x: xl, y: pl.body.minY - 5.08 - extra }, { x: xl, y: pl.body.minY - 2.54 - extra }], [{ x: xr, y: pl.body.minY - 5.08 - extra }, { x: xr, y: pl.body.minY - 2.54 - extra }], [{ x: xl, y: pl.body.maxY + 2.54 + extra }, { x: xl, y: pl.body.maxY + 5.08 + extra }], [{ x: xr, y: pl.body.maxY + 2.54 + extra }, { x: xr, y: pl.body.maxY + 5.08 + extra }], [
|
|
1969
4306
|
{ x: pl.body.maxX + textW / 2 + 1.27 + extra, y: cy - 1.27 },
|
|
1970
4307
|
{ x: pl.body.maxX + textW / 2 + 1.27 + extra, y: cy + 1.27 },
|
|
1971
4308
|
], [
|
|
@@ -1973,13 +4310,17 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
1973
4310
|
{ x: pl.body.minX - textW / 2 - 1.27 - extra, y: cy + 1.27 },
|
|
1974
4311
|
]);
|
|
1975
4312
|
}
|
|
4313
|
+
let slotted = false;
|
|
1976
4314
|
for (const [r, v] of ladder) {
|
|
1977
4315
|
if (pairClear(r, v)) {
|
|
1978
4316
|
sym.refAt = r;
|
|
1979
4317
|
sym.valueAt = v;
|
|
4318
|
+
slotted = true;
|
|
1980
4319
|
break;
|
|
1981
4320
|
}
|
|
1982
4321
|
}
|
|
4322
|
+
if (!slotted)
|
|
4323
|
+
trace(`fields of ${dref}: no clear slot in ${ladder.length} rungs; heuristic kept`);
|
|
1983
4324
|
}
|
|
1984
4325
|
fieldBoxes.push(centeredTextBox(dref, sym.refAt.x, sym.refAt.y), centeredTextBox(sym.value, sym.valueAt.x, sym.valueAt.y));
|
|
1985
4326
|
}
|
|
@@ -2015,24 +4356,95 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2015
4356
|
// are the label's own attachment and never count as collisions.
|
|
2016
4357
|
for (const rec of wiredLabels) {
|
|
2017
4358
|
const lb = labels[rec.label];
|
|
2018
|
-
const clearWired = (x, y) => {
|
|
2019
|
-
const box = labelTextBox(lb.name, x, y,
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
4359
|
+
const clearWired = (x, y, rot, pad = TEXT_PAD) => {
|
|
4360
|
+
const box = labelTextBox(lb.name, x, y, rot, lb.kind);
|
|
4361
|
+
const padBox = (b) => ({ minX: b.minX - pad, minY: b.minY - pad, maxX: b.maxX + pad, maxY: b.maxY + pad });
|
|
4362
|
+
const blocked = (why) => {
|
|
4363
|
+
trace(`label ${lb.name} at (${x}, ${y}, ${rot}) blocked by ${why}`);
|
|
2023
4364
|
return false;
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
4365
|
+
};
|
|
4366
|
+
// A wire through the anchor is the label's own attachment — for a
|
|
4367
|
+
// plain label, whose text stands above the line. A flag is drawn
|
|
4368
|
+
// CENTRED on its anchor line, so a wire continuing under its text
|
|
4369
|
+
// runs straight through the flag; only the wire on the pole side
|
|
4370
|
+
// (behind the flag's tip) is its attachment.
|
|
4371
|
+
const attached = (w) => {
|
|
4372
|
+
if (!segContains(w, x, y))
|
|
4373
|
+
return false;
|
|
4374
|
+
if (lb.kind !== 'global')
|
|
4375
|
+
return true;
|
|
4376
|
+
const r = ((rot % 360) + 360) % 360;
|
|
4377
|
+
if (r === 0)
|
|
4378
|
+
return Math.max(w.x1, w.x2) <= x + 0.01;
|
|
4379
|
+
if (r === 180)
|
|
4380
|
+
return Math.min(w.x1, w.x2) >= x - 0.01;
|
|
4381
|
+
if (r === 90)
|
|
4382
|
+
return Math.min(w.y1, w.y2) >= y - 0.01;
|
|
4383
|
+
return Math.max(w.y1, w.y2) <= y + 0.01;
|
|
4384
|
+
};
|
|
4385
|
+
const hitBody = [...placed.entries()].find(([, pl]) => boundsOverlap(box, pl.body));
|
|
4386
|
+
if (hitBody)
|
|
4387
|
+
return blocked(`the body of ${hitBody[0]} (${JSON.stringify(hitBody[1].body)})`);
|
|
4388
|
+
if (textObstacles.some((b) => boundsOverlap(padBox(box), b)))
|
|
4389
|
+
return blocked('field text');
|
|
4390
|
+
if (wires.some((w) => !attached(w) && segHitsBox(w, box)))
|
|
4391
|
+
return blocked('a wire');
|
|
4392
|
+
return !labels.some((o, i) => i !== rec.label && o.name !== lb.name && boundsOverlap(padBox(box), labelTextBox(o.name, o.x, o.y, o.rot, o.kind)));
|
|
2027
4393
|
};
|
|
2028
|
-
if (clearWired(lb.x, lb.y))
|
|
4394
|
+
if (clearWired(lb.x, lb.y, lb.rot))
|
|
2029
4395
|
continue;
|
|
2030
|
-
|
|
4396
|
+
trace(`label ${lb.name}: (${lb.x}, ${lb.y}, ${lb.rot}) not clear, searching`);
|
|
4397
|
+
// A flag may turn as well as move — its candidates each carry a rotation
|
|
4398
|
+
// — and prefers a trunk top; a local name stays horizontal and walks the
|
|
4399
|
+
// run's points in anchor-preference order.
|
|
4400
|
+
if (lb.kind === 'global') {
|
|
4401
|
+
// a run's name reads along the row (the drafting standard keeps text
|
|
4402
|
+
// horizontal); a flag that could only stand up leaves the net to
|
|
4403
|
+
// local labels below
|
|
4404
|
+
const alt = wiredFlagCandidates(rec.segs)
|
|
4405
|
+
.filter((c) => c.rot === 0 || c.rot === 180)
|
|
4406
|
+
.find((c) => clearWired(c.x, c.y, c.rot));
|
|
4407
|
+
if (alt) {
|
|
4408
|
+
lb.x = alt.x;
|
|
4409
|
+
lb.y = alt.y;
|
|
4410
|
+
lb.rot = alt.rot;
|
|
4411
|
+
continue;
|
|
4412
|
+
}
|
|
4413
|
+
// A flag that fits nowhere on its run (a part on a pin row of a
|
|
4414
|
+
// 2.54 mm-pitch IC has no free end and no room for a flag standing up
|
|
4415
|
+
// between the rows) leaves the net named by plain labels instead:
|
|
4416
|
+
// every flag of this net becomes a local label, which KiCad connects
|
|
4417
|
+
// across the sheet just the same, and the run's name goes back to
|
|
4418
|
+
// standing on its wire.
|
|
4419
|
+
for (const o of labels) {
|
|
4420
|
+
if (o.name !== lb.name)
|
|
4421
|
+
continue;
|
|
4422
|
+
o.kind = 'local';
|
|
4423
|
+
delete o.shape;
|
|
4424
|
+
if (o.rot === 90 || o.rot === 270)
|
|
4425
|
+
o.rot = 0; // plain text reads along the row
|
|
4426
|
+
}
|
|
4427
|
+
lb.rot = 0;
|
|
4428
|
+
trace(`label ${lb.name}: no flag position clears; the net keeps local labels`);
|
|
4429
|
+
}
|
|
4430
|
+
const alt = rec.pts.find((p) => clearWired(p.x, p.y, 0));
|
|
2031
4431
|
if (alt) {
|
|
4432
|
+
trace(`label ${lb.name} moved along its run (${lb.x}, ${lb.y}) -> (${alt.x}, ${alt.y})`);
|
|
2032
4433
|
lb.x = alt.x;
|
|
2033
4434
|
lb.y = alt.y;
|
|
2034
4435
|
continue;
|
|
2035
4436
|
}
|
|
4437
|
+
// reading leftward from a point on the run is the same text on the same
|
|
4438
|
+
// wire; a run whose right-hand end is a part's body (a resistor on the
|
|
4439
|
+
// row) often clears only that way
|
|
4440
|
+
const altLeft = [...rec.pts, ...interiorGridPoints(rec.segs, U)].find((p) => clearWired(p.x, p.y, 180));
|
|
4441
|
+
if (altLeft) {
|
|
4442
|
+
trace(`label ${lb.name} reads leftward from (${altLeft.x}, ${altLeft.y})`);
|
|
4443
|
+
lb.x = altLeft.x;
|
|
4444
|
+
lb.y = altLeft.y;
|
|
4445
|
+
lb.rot = 180;
|
|
4446
|
+
continue;
|
|
4447
|
+
}
|
|
2036
4448
|
// No segment endpoint clears, but the label may sit at ANY point of its
|
|
2037
4449
|
// own net's wires: walk the interior grid points of each segment too
|
|
2038
4450
|
// (#220 phase 2). Endpoints stay the first choice so a run that used to
|
|
@@ -2045,20 +4457,39 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2045
4457
|
// one the IR gave it. Three corpus nets shipped exactly that way —
|
|
2046
4458
|
// Net-(F201-Pad1), Net-(U8-BIN) and Net-(U8-RIN), each anchored on no
|
|
2047
4459
|
// wire of its own run.
|
|
2048
|
-
const inner = interiorGridPoints(rec.segs, U).find((p) => clearWired(p.x, p.y));
|
|
4460
|
+
const inner = interiorGridPoints(rec.segs, U).find((p) => clearWired(p.x, p.y, 0));
|
|
2049
4461
|
if (inner) {
|
|
4462
|
+
trace(`label ${lb.name} moved to an interior point (${inner.x}, ${inner.y})`);
|
|
2050
4463
|
lb.x = inner.x;
|
|
2051
4464
|
lb.y = inner.y;
|
|
4465
|
+
continue;
|
|
4466
|
+
}
|
|
4467
|
+
// Last resort before leaving the name on a body: the full pad cannot be
|
|
4468
|
+
// had on a 2.54 mm pin row under a flag (a flag's half height plus a
|
|
4469
|
+
// standing label's height already fill the pitch), so accept a quarter
|
|
4470
|
+
// pad at the first point that clears everything else.
|
|
4471
|
+
// (A flag on the row above already reaches a full height below its line,
|
|
4472
|
+
// so against it even a quarter pad is too much; touching boxes are not a
|
|
4473
|
+
// collision to the checker, and are the last rung.)
|
|
4474
|
+
const tightCandidates = [...rec.pts.map((p) => ({ ...p, rot: 0 })), ...interiorGridPoints(rec.segs, U).map((p) => ({ ...p, rot: 0 })), ...rec.pts.map((p) => ({ ...p, rot: 180 }))];
|
|
4475
|
+
const tight = tightCandidates.find((c) => clearWired(c.x, c.y, c.rot, TEXT_PAD / 4)) ?? tightCandidates.find((c) => clearWired(c.x, c.y, c.rot, 0));
|
|
4476
|
+
if (tight) {
|
|
4477
|
+
trace(`label ${lb.name} placed at (${tight.x}, ${tight.y}, ${tight.rot}) with a reduced pad`);
|
|
4478
|
+
lb.x = tight.x;
|
|
4479
|
+
lb.y = tight.y;
|
|
4480
|
+
lb.rot = tight.rot;
|
|
4481
|
+
continue;
|
|
2052
4482
|
}
|
|
4483
|
+
trace(`label ${lb.name}: no clear point on its run; left at (${lb.x}, ${lb.y})`);
|
|
2053
4484
|
}
|
|
2054
4485
|
for (const rec of stubbedLabels) {
|
|
2055
4486
|
const lb = labels[rec.label];
|
|
2056
4487
|
const stub = wires[rec.wire];
|
|
2057
4488
|
const clearAt = (x, y, rot = lb.rot) => {
|
|
2058
|
-
const box = labelTextBox(lb.name, x, y, rot);
|
|
4489
|
+
const box = labelTextBox(lb.name, x, y, rot, lb.kind);
|
|
2059
4490
|
if (bodies.some((b) => boundsOverlap(box, b)))
|
|
2060
4491
|
return false;
|
|
2061
|
-
if (textObstacles.some((b) => boundsOverlap(box, b)))
|
|
4492
|
+
if (textObstacles.some((b) => boundsOverlap(padBox(box), b)))
|
|
2062
4493
|
return false;
|
|
2063
4494
|
if (wires.some((w, i) => i !== rec.wire && segHitsBox(w, box)))
|
|
2064
4495
|
return false;
|
|
@@ -2070,9 +4501,10 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2070
4501
|
// coordinates. Read live from `labels`, so already-nudged neighbours are
|
|
2071
4502
|
// seen at their final positions and immovable wired-net labels (which
|
|
2072
4503
|
// carry no stub to ride) are seen at all.
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
4504
|
+
// A label of the SAME net is still text: two BUCK_EN labels one over
|
|
4505
|
+
// the other read as one smudge and the checker flags them (a hung
|
|
4506
|
+
// part's stub label rode down onto the IC pin's).
|
|
4507
|
+
return !labels.some((o, i) => i !== rec.label && boundsOverlap(padBox(box), labelTextBox(o.name, o.x, o.y, o.rot, o.kind)));
|
|
2076
4508
|
};
|
|
2077
4509
|
/**
|
|
2078
4510
|
* Does another net's label sit on exactly this point? That is the fatal
|
|
@@ -2096,11 +4528,24 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2096
4528
|
predictStubs: false,
|
|
2097
4529
|
});
|
|
2098
4530
|
const rideTo = (x, y) => {
|
|
4531
|
+
if (!sameCoord(x, lb.x) || !sameCoord(y, lb.y))
|
|
4532
|
+
trace(`label ${lb.name}: rides its stub from (${lb.x}, ${lb.y}) to (${x}, ${y})`);
|
|
2099
4533
|
stub.x2 = x;
|
|
2100
4534
|
stub.y2 = y;
|
|
2101
4535
|
lb.x = x;
|
|
2102
4536
|
lb.y = y;
|
|
2103
4537
|
};
|
|
4538
|
+
// A flag on a vertical stub reads along the row when nothing stands
|
|
4539
|
+
// there (the drafting standard keeps text horizontal; the checker names
|
|
4540
|
+
// every rotated label a horizontal one would have fitted).
|
|
4541
|
+
if (lb.kind === 'global' && rec.o.dy !== 0 && (lb.rot === 90 || lb.rot === 270)) {
|
|
4542
|
+
const flat = [0, 180].find((r) => clearAt(lb.x, lb.y, r) && !mergesAt(lb.x, lb.y));
|
|
4543
|
+
if (flat !== undefined) {
|
|
4544
|
+
lb.rot = flat;
|
|
4545
|
+
continue;
|
|
4546
|
+
}
|
|
4547
|
+
trace(`flag ${lb.name} at (${lb.x}, ${lb.y}) stays vertical: no horizontal draw clears`);
|
|
4548
|
+
}
|
|
2104
4549
|
if (clearAt(lb.x, lb.y))
|
|
2105
4550
|
continue;
|
|
2106
4551
|
/** Candidate points along the stub, nearest first. */
|
|
@@ -2139,6 +4584,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2139
4584
|
!mergesAt(c.x, c.y) &&
|
|
2140
4585
|
(sameCoord(c.x, lb.x) && sameCoord(c.y, lb.y) ? true : wireClearAt(c.x, c.y)));
|
|
2141
4586
|
if (flip) {
|
|
4587
|
+
trace(`label ${lb.name}: turns to rotation ${flipRot} to clear`);
|
|
2142
4588
|
lb.rot = flipRot;
|
|
2143
4589
|
rideTo(flip.x, flip.y);
|
|
2144
4590
|
continue;
|
|
@@ -2171,7 +4617,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2171
4617
|
// measure as clean. Nothing clear keeps the placed slot so the report stays
|
|
2172
4618
|
// honest, and a value that is already clean does not move at all.
|
|
2173
4619
|
{
|
|
2174
|
-
const labelBoxesFinal = labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot));
|
|
4620
|
+
const labelBoxesFinal = labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot, l.kind));
|
|
2175
4621
|
const memberText = emitPairs.flatMap(({ sym: s, pl }) => [
|
|
2176
4622
|
centeredTextBox(displayRefOf(pl), s.refAt.x, s.refAt.y),
|
|
2177
4623
|
centeredTextBox(s.value, s.valueAt.x, s.valueAt.y),
|
|
@@ -2180,9 +4626,9 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2180
4626
|
const liveBoxes = new Map(valueEntries.map((s) => [s, powerValueBox(s.value, s.valueAt.x, s.valueAt.y)]));
|
|
2181
4627
|
const clearFor = (self, b) => !bodies.some((bd) => boundsOverlap(b, bd)) &&
|
|
2182
4628
|
!wires.some((w) => segHitsBoxEarly(w, b)) &&
|
|
2183
|
-
!labelBoxesFinal.some((lb) => boundsOverlap(lb, b)) &&
|
|
2184
|
-
!memberText.some((t) => boundsOverlap(t, b)) &&
|
|
2185
|
-
![...liveBoxes].some(([o, ob]) => o !== self && boundsOverlap(ob, b));
|
|
4629
|
+
!labelBoxesFinal.some((lb) => boundsOverlap(lb, padBox(b))) &&
|
|
4630
|
+
!memberText.some((t) => boundsOverlap(t, padBox(b))) &&
|
|
4631
|
+
![...liveBoxes].some(([o, ob]) => o !== self && boundsOverlap(ob, padBox(b)));
|
|
2186
4632
|
for (const s of valueEntries) {
|
|
2187
4633
|
if (clearFor(s, liveBoxes.get(s)))
|
|
2188
4634
|
continue;
|
|
@@ -2232,6 +4678,470 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2232
4678
|
}
|
|
2233
4679
|
}
|
|
2234
4680
|
// ---------- sheet: content-derived paper, balanced placement ----------
|
|
4681
|
+
// ---------- group boxes enclose their text ----------
|
|
4682
|
+
// what the wrap was fitted on, to measure how far the text grew each box
|
|
4683
|
+
const rectsBeforeText = groupRects.map((r) => ({ name: r.name, x1: r.x1, x2: r.x2, y1: r.y1, y2: r.y2 }));
|
|
4684
|
+
const reachOut = new Map();
|
|
4685
|
+
// A box drawn from bodies plus a fixed margin cuts through the text its
|
|
4686
|
+
// parts carry: with the IC in the leftmost column, its left-facing pin
|
|
4687
|
+
// labels ran out through the box edge (EN, BTN_PLAY, SPK_L+ on esp32-amp),
|
|
4688
|
+
// and a rail name on a connector stub crossed the line on every reference
|
|
4689
|
+
// board. Grow each box to hold its members' stub labels, their reference
|
|
4690
|
+
// and value fields, and the power symbols on their pins, plus BOX_PAD; the
|
|
4691
|
+
// group gaps above were widened for exactly this text, so boxes stay clear
|
|
4692
|
+
// of one another.
|
|
4693
|
+
{
|
|
4694
|
+
const pinGroup = new Map();
|
|
4695
|
+
for (const [key, pl] of placed) {
|
|
4696
|
+
const g = groupOf.get(key);
|
|
4697
|
+
if (!g)
|
|
4698
|
+
continue;
|
|
4699
|
+
for (const pin of pl.sym.pins) {
|
|
4700
|
+
const p = pinAt(pl, pin);
|
|
4701
|
+
pinGroup.set(pointKey(p.x, p.y), g);
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
const boxesOf = new Map();
|
|
4705
|
+
const addBox = (g, b) => {
|
|
4706
|
+
if (!g)
|
|
4707
|
+
return;
|
|
4708
|
+
boxesOf.set(g, [...(boxesOf.get(g) ?? []), b]);
|
|
4709
|
+
};
|
|
4710
|
+
// a label at the end of a stub that starts on a member pin
|
|
4711
|
+
for (const lb of labels) {
|
|
4712
|
+
const stub = wires.find((w) => sameCoord(w.x2, lb.x) && sameCoord(w.y2, lb.y) && pinGroup.has(pointKey(w.x1, w.y1)));
|
|
4713
|
+
const g = stub ? pinGroup.get(pointKey(stub.x1, stub.y1)) : undefined;
|
|
4714
|
+
addBox(g, labelReserveBox(lb.name, lb.x, lb.y, lb.rot, lb.kind));
|
|
4715
|
+
}
|
|
4716
|
+
// reference and value fields of every member, at the reserve advance
|
|
4717
|
+
const fieldBox = (text, x, y) => {
|
|
4718
|
+
const w = Math.max(1, text.length) * TEXT_RESERVE * LABEL_HEIGHT;
|
|
4719
|
+
return { minX: x - w / 2, minY: y - LABEL_HEIGHT / 2, maxX: x + w / 2, maxY: y + LABEL_HEIGHT / 2 };
|
|
4720
|
+
};
|
|
4721
|
+
const keyOfPlaced = new Map([...placed.entries()].map(([k, p]) => [p, k]));
|
|
4722
|
+
for (const { sym, pl } of emitPairs) {
|
|
4723
|
+
const g = groupOf.get(keyOfPlaced.get(pl) ?? '');
|
|
4724
|
+
addBox(g, fieldBox(displayRefOf(pl), sym.refAt.x, sym.refAt.y));
|
|
4725
|
+
addBox(g, fieldBox(sym.value, sym.valueAt.x, sym.valueAt.y));
|
|
4726
|
+
}
|
|
4727
|
+
// power symbols on member stubs, with their value text
|
|
4728
|
+
for (const ps of extraSymbols) {
|
|
4729
|
+
const stub = wires.find((w) => sameCoord(w.x2, ps.at.x) && sameCoord(w.y2, ps.at.y) && pinGroup.has(pointKey(w.x1, w.y1)));
|
|
4730
|
+
const g = stub ? pinGroup.get(pointKey(stub.x1, stub.y1)) : undefined;
|
|
4731
|
+
if (!g)
|
|
4732
|
+
continue;
|
|
4733
|
+
addBox(g, { minX: ps.at.x - 2 * U, minY: ps.at.y - 2 * U, maxX: ps.at.x + 2 * U, maxY: ps.at.y + 2 * U });
|
|
4734
|
+
if (!ps.hideValue)
|
|
4735
|
+
addBox(g, powerValueBox(ps.value, ps.valueAt.x, ps.valueAt.y));
|
|
4736
|
+
}
|
|
4737
|
+
// every member body too: a hung part on a shared lane or an infilled
|
|
4738
|
+
// cell may sit past the cells the rect was first drawn from
|
|
4739
|
+
for (const [key, pl] of placed) {
|
|
4740
|
+
const g = groupOf.get(key);
|
|
4741
|
+
if (g)
|
|
4742
|
+
addBox(g, pl.body);
|
|
4743
|
+
}
|
|
4744
|
+
for (const r of groupRects) {
|
|
4745
|
+
for (const b of boxesOf.get(r.name) ?? []) {
|
|
4746
|
+
r.x1 = Math.min(r.x1, b.minX - BOX_PAD * U);
|
|
4747
|
+
r.x2 = Math.max(r.x2, b.maxX + BOX_PAD * U);
|
|
4748
|
+
r.y1 = Math.min(r.y1, b.minY - BOX_PAD * U);
|
|
4749
|
+
r.y2 = Math.max(r.y2, b.maxY + BOX_PAD * U);
|
|
4750
|
+
}
|
|
4751
|
+
// The caption is a band across the top of the box, not a corner the
|
|
4752
|
+
// parts may rise into: nothing drawn starts above the caption's
|
|
4753
|
+
// bottom plus a unit of air, whatever its column (a rail name beside
|
|
4754
|
+
// the caption read as part of it).
|
|
4755
|
+
const contentTop = Math.min(...(boxesOf.get(r.name) ?? []).map((b) => b.minY));
|
|
4756
|
+
if (Number.isFinite(contentTop))
|
|
4757
|
+
r.y1 = Math.min(r.y1, contentTop - (2 + CAPTION_SIZE + U));
|
|
4758
|
+
r.y1 = grid(Math.floor(r.y1 / U));
|
|
4759
|
+
}
|
|
4760
|
+
// measured before the boxes are aligned: alignment is a choice, not text
|
|
4761
|
+
for (const [i, r] of groupRects.entries()) {
|
|
4762
|
+
const before = rectsBeforeText[i];
|
|
4763
|
+
const applied = reachMeasured?.get(r.name);
|
|
4764
|
+
reachOut.set(r.name, {
|
|
4765
|
+
left: Math.max(0, before.x1 - r.x1),
|
|
4766
|
+
right: Math.max(0, r.x2 - before.x2),
|
|
4767
|
+
top: (applied?.top ?? 0) + Math.max(0, before.y1 - r.y1),
|
|
4768
|
+
bottom: (applied?.bottom ?? 0) + Math.max(0, r.y2 - before.y2),
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
// Boxes in one row of the wrap share a bottom edge, boxes in one column
|
|
4772
|
+
// a right edge, when the neighbour's space is free anyway: a row of
|
|
4773
|
+
// boxes ending at three different heights reads as three afterthoughts.
|
|
4774
|
+
// A line of the wrap (a row, or a column) is the set of boxes whose
|
|
4775
|
+
// fitted rects landed at one top (one left): the shifts that took them
|
|
4776
|
+
// there differ box by box, since the single-row pass gave them
|
|
4777
|
+
// different tops.
|
|
4778
|
+
const columnar = fit.wrap?.kind === 'columns' || fit.wrap?.kind === 'masonry';
|
|
4779
|
+
// Two boxes share a line when their fitted rects overlap across it (in
|
|
4780
|
+
// height for a row, in width for a column): a row's boxes may start at
|
|
4781
|
+
// different tops (the single-row pass gives a box room above its IC for
|
|
4782
|
+
// a pull-up) and two rows never overlap, the wrap having left a gap.
|
|
4783
|
+
const lineIndex = new Array(groupRects.length).fill(-1);
|
|
4784
|
+
{
|
|
4785
|
+
const lo = (i) => (columnar ? rectsBeforeText[i].x1 : rectsBeforeText[i].y1);
|
|
4786
|
+
const hi = (i) => (columnar ? rectsBeforeText[i].x2 : rectsBeforeText[i].y2);
|
|
4787
|
+
const order = groupRects.map((_, i) => i).sort((a, b) => lo(a) - lo(b));
|
|
4788
|
+
let line = -1;
|
|
4789
|
+
let reach = -Infinity;
|
|
4790
|
+
for (const i of order) {
|
|
4791
|
+
if (lo(i) >= reach - 1e-6)
|
|
4792
|
+
line++;
|
|
4793
|
+
lineIndex[i] = line;
|
|
4794
|
+
reach = Math.max(reach, hi(i));
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
const lineOf = (i) => lineIndex[i];
|
|
4798
|
+
if (fit.wrap) {
|
|
4799
|
+
const byLine = new Map();
|
|
4800
|
+
for (const [i, r] of groupRects.entries()) {
|
|
4801
|
+
byLine.set(lineOf(i), [...(byLine.get(lineOf(i)) ?? []), r]);
|
|
4802
|
+
}
|
|
4803
|
+
for (const line of byLine.values()) {
|
|
4804
|
+
if (line.length < 2)
|
|
4805
|
+
continue;
|
|
4806
|
+
if (fit.wrap.kind === 'columns' || fit.wrap.kind === 'masonry') {
|
|
4807
|
+
const right = Math.max(...line.map((r) => r.x2));
|
|
4808
|
+
for (const r of line)
|
|
4809
|
+
r.x2 = right;
|
|
4810
|
+
}
|
|
4811
|
+
else {
|
|
4812
|
+
const bottom = Math.max(...line.map((r) => r.y2));
|
|
4813
|
+
for (const r of line)
|
|
4814
|
+
r.y2 = bottom;
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
// The boxes grew into the gaps the wrap left between cells, each by its
|
|
4819
|
+
// own text, so two boxes might nearly touch where two others stood 10 mm
|
|
4820
|
+
// apart, and a row's boxes started at three different heights. Every box
|
|
4821
|
+
// now moves, with everything drawn in it, so that BOX_LINE_GAP separates
|
|
4822
|
+
// neighbours along and across the lines of the wrap, and a row shares
|
|
4823
|
+
// its top (a column its left). Content moves by whole units to keep the
|
|
4824
|
+
// grid; the box takes the exact position, so its padding varies by less
|
|
4825
|
+
// than a unit while the gaps do not.
|
|
4826
|
+
{
|
|
4827
|
+
const lineGap = BOX_LINE_GAP * U;
|
|
4828
|
+
const byLine = new Map();
|
|
4829
|
+
for (const [i, r] of groupRects.entries()) {
|
|
4830
|
+
byLine.set(lineOf(i), [...(byLine.get(lineOf(i)) ?? []), { r, i }]);
|
|
4831
|
+
}
|
|
4832
|
+
// Every drawn item belongs to one group, decided once before anything
|
|
4833
|
+
// moves: a wire, label, junction, no-connect or power symbol to the
|
|
4834
|
+
// group of the pins on its run (wires joined end to end form the run),
|
|
4835
|
+
// anything unwired to the box it lies in. Deciding by box while boxes
|
|
4836
|
+
// move would hand a moved group's items to the next box they land in.
|
|
4837
|
+
const parent = new Map();
|
|
4838
|
+
const find = (k) => {
|
|
4839
|
+
let x = k;
|
|
4840
|
+
while (parent.has(x) && parent.get(x) !== x)
|
|
4841
|
+
x = parent.get(x);
|
|
4842
|
+
return x;
|
|
4843
|
+
};
|
|
4844
|
+
const union = (a, b) => {
|
|
4845
|
+
if (!parent.has(a))
|
|
4846
|
+
parent.set(a, a);
|
|
4847
|
+
if (!parent.has(b))
|
|
4848
|
+
parent.set(b, b);
|
|
4849
|
+
const ra = find(a);
|
|
4850
|
+
const rb = find(b);
|
|
4851
|
+
if (ra !== rb)
|
|
4852
|
+
parent.set(ra, rb);
|
|
4853
|
+
};
|
|
4854
|
+
for (const w of wires)
|
|
4855
|
+
union(pointKey(w.x1, w.y1), pointKey(w.x2, w.y2));
|
|
4856
|
+
const runGroup = new Map();
|
|
4857
|
+
for (const [pt, g] of pinGroup) {
|
|
4858
|
+
const root = find(pt);
|
|
4859
|
+
if (!runGroup.has(root))
|
|
4860
|
+
runGroup.set(root, g);
|
|
4861
|
+
}
|
|
4862
|
+
const inside = (x, y, r) => x >= r.x1 - 1e-6 && x <= r.x2 + 1e-6 && y >= r.y1 - 1e-6 && y <= r.y2 + 1e-6;
|
|
4863
|
+
const groupAt = (x, y) => runGroup.get(find(pointKey(x, y))) ?? groupRects.find((r) => inside(x, y, r))?.name;
|
|
4864
|
+
const wireGroup = new Map(wires.map((w) => [w, groupAt(w.x1, w.y1) ?? groupAt(w.x2, w.y2)]));
|
|
4865
|
+
const pointGroup = new Map();
|
|
4866
|
+
for (const it of [...labels, ...uniqJunctions, ...noConnects])
|
|
4867
|
+
pointGroup.set(it, groupAt(it.x, it.y));
|
|
4868
|
+
const powerGroup = new Map(extraSymbols.map((ps) => [ps, groupAt(ps.at.x, ps.at.y)]));
|
|
4869
|
+
const moveGroup = (r, dx, dy) => {
|
|
4870
|
+
if (!dx && !dy)
|
|
4871
|
+
return;
|
|
4872
|
+
for (const [key, pl] of placed) {
|
|
4873
|
+
if (groupOf.get(key) !== r.name)
|
|
4874
|
+
continue;
|
|
4875
|
+
pl.x += dx;
|
|
4876
|
+
pl.y += dy;
|
|
4877
|
+
pl.body.minX += dx;
|
|
4878
|
+
pl.body.maxX += dx;
|
|
4879
|
+
pl.body.minY += dy;
|
|
4880
|
+
pl.body.maxY += dy;
|
|
4881
|
+
}
|
|
4882
|
+
for (const { sym, pl } of emitPairs) {
|
|
4883
|
+
if (groupOf.get(keyOfPlaced.get(pl) ?? '') !== r.name)
|
|
4884
|
+
continue;
|
|
4885
|
+
sym.at.x += dx;
|
|
4886
|
+
sym.at.y += dy;
|
|
4887
|
+
sym.refAt.x += dx;
|
|
4888
|
+
sym.refAt.y += dy;
|
|
4889
|
+
sym.valueAt.x += dx;
|
|
4890
|
+
sym.valueAt.y += dy;
|
|
4891
|
+
}
|
|
4892
|
+
for (const ps of extraSymbols) {
|
|
4893
|
+
if (powerGroup.get(ps) !== r.name)
|
|
4894
|
+
continue;
|
|
4895
|
+
ps.at.x += dx;
|
|
4896
|
+
ps.at.y += dy;
|
|
4897
|
+
ps.valueAt.x += dx;
|
|
4898
|
+
ps.valueAt.y += dy;
|
|
4899
|
+
}
|
|
4900
|
+
for (const w of wires) {
|
|
4901
|
+
if (wireGroup.get(w) !== r.name)
|
|
4902
|
+
continue;
|
|
4903
|
+
w.x1 += dx;
|
|
4904
|
+
w.x2 += dx;
|
|
4905
|
+
w.y1 += dy;
|
|
4906
|
+
w.y2 += dy;
|
|
4907
|
+
}
|
|
4908
|
+
for (const it of [...labels, ...uniqJunctions, ...noConnects]) {
|
|
4909
|
+
if (pointGroup.get(it) !== r.name)
|
|
4910
|
+
continue;
|
|
4911
|
+
it.x += dx;
|
|
4912
|
+
it.y += dy;
|
|
4913
|
+
}
|
|
4914
|
+
r.x1 += dx;
|
|
4915
|
+
r.x2 += dx;
|
|
4916
|
+
r.y1 += dy;
|
|
4917
|
+
r.y2 += dy;
|
|
4918
|
+
};
|
|
4919
|
+
const snap = (d) => grid(Math.round(d / U));
|
|
4920
|
+
const lines = [...byLine.entries()].sort((a, b) => a[0] - b[0]).map(([, members]) => members);
|
|
4921
|
+
const left0 = Math.min(...groupRects.map((r) => r.x1));
|
|
4922
|
+
const top0 = Math.min(...groupRects.map((r) => r.y1));
|
|
4923
|
+
// what each box was before the alignment above shared its edges
|
|
4924
|
+
const ownSize = new Map(groupRects.map((r, i) => [r.name, { w: Math.max(...(boxesOf.get(r.name) ?? []).map((b) => b.maxX + BOX_PAD * U), rectsBeforeText[i].x2) - r.x1, h: Math.max(...(boxesOf.get(r.name) ?? []).map((b) => b.maxY + BOX_PAD * U), rectsBeforeText[i].y2) - r.y1 }]));
|
|
4925
|
+
if (columnar) {
|
|
4926
|
+
let colLeft = left0;
|
|
4927
|
+
for (const members of lines) {
|
|
4928
|
+
members.sort((a, b) => a.r.y1 - b.r.y1);
|
|
4929
|
+
const colW = Math.max(...members.map((m) => m.r.x2 - m.r.x1));
|
|
4930
|
+
let y = top0;
|
|
4931
|
+
for (const { r } of members) {
|
|
4932
|
+
const h = r.y2 - r.y1;
|
|
4933
|
+
moveGroup(r, snap(colLeft - r.x1), snap(y - r.y1));
|
|
4934
|
+
r.x1 = colLeft;
|
|
4935
|
+
r.x2 = colLeft + colW;
|
|
4936
|
+
r.y1 = y;
|
|
4937
|
+
r.y2 = y + h;
|
|
4938
|
+
y += h + lineGap;
|
|
4939
|
+
}
|
|
4940
|
+
colLeft += colW + lineGap;
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
else {
|
|
4944
|
+
let rowTop = top0;
|
|
4945
|
+
for (const members of lines) {
|
|
4946
|
+
members.sort((a, b) => a.r.x1 - b.r.x1);
|
|
4947
|
+
const rowH = Math.max(...members.map((m) => m.r.y2 - m.r.y1));
|
|
4948
|
+
let x = left0;
|
|
4949
|
+
for (const { r } of members) {
|
|
4950
|
+
const w = r.x2 - r.x1;
|
|
4951
|
+
moveGroup(r, snap(x - r.x1), snap(rowTop - r.y1));
|
|
4952
|
+
r.x1 = x;
|
|
4953
|
+
r.x2 = x + w;
|
|
4954
|
+
r.y1 = rowTop;
|
|
4955
|
+
r.y2 = rowTop + rowH;
|
|
4956
|
+
x += w + lineGap;
|
|
4957
|
+
}
|
|
4958
|
+
rowTop += rowH + lineGap;
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
// A shared edge may reach where the box's own text did not: the
|
|
4962
|
+
// title-block corner. The sheet pass below centres the content in the
|
|
4963
|
+
// frame; predicted here the same way, any box whose aligned edge would
|
|
4964
|
+
// enter the corner falls back to its own width or height there.
|
|
4965
|
+
{
|
|
4966
|
+
const paper = fit.paper;
|
|
4967
|
+
const xs = groupRects.flatMap((r) => [r.x1, r.x2]);
|
|
4968
|
+
const ys = groupRects.flatMap((r) => [r.y1, r.y2]);
|
|
4969
|
+
const contentW = Math.max(...xs) - Math.min(...xs);
|
|
4970
|
+
const contentH = Math.max(...ys) - Math.min(...ys);
|
|
4971
|
+
const dx = grid(Math.round((FRAME + Math.max(0, (paper.w - 2 * FRAME - contentW) / 2) - Math.min(...xs)) / U));
|
|
4972
|
+
const dy = grid(Math.round((FRAME + 4 * U + Math.max(0, (paper.h - 2 * FRAME - TITLE_STRIP - contentH) / 2) - Math.min(...ys)) / U));
|
|
4973
|
+
const cornerX = paper.w - FRAME - TITLE_BLOCK_W - dx;
|
|
4974
|
+
const cornerY = paper.h - FRAME - TITLE_STRIP - dy;
|
|
4975
|
+
for (const r of groupRects) {
|
|
4976
|
+
if (r.x2 <= cornerX || r.y2 <= cornerY)
|
|
4977
|
+
continue;
|
|
4978
|
+
const o = ownSize.get(r.name);
|
|
4979
|
+
const ownX2 = r.x1 + o.w;
|
|
4980
|
+
const ownY2 = r.y1 + o.h;
|
|
4981
|
+
// give back the aligned width first (a wide short row), then the height
|
|
4982
|
+
if (ownX2 <= cornerX && r.x2 > ownX2)
|
|
4983
|
+
r.x2 = ownX2;
|
|
4984
|
+
if (r.x2 > cornerX && ownY2 <= cornerY && r.y2 > ownY2)
|
|
4985
|
+
r.y2 = ownY2;
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
// ---------- measure what every cell drew (for the squeeze), before the sheet shift ----------
|
|
4991
|
+
// Every drawn item is attributed to the cells it belongs to: a body and its
|
|
4992
|
+
// fields to their instance; a wire, with the labels and power symbols on
|
|
4993
|
+
// it, to every instance with a pin on its run (a comb belongs to the IC and
|
|
4994
|
+
// the parts hung on it, which roll up to the IC below). A cell's overhang is
|
|
4995
|
+
// then how far that union reaches past its body on each side.
|
|
4996
|
+
const measuredOut = new Map();
|
|
4997
|
+
{
|
|
4998
|
+
const parent = new Map();
|
|
4999
|
+
const find = (k) => {
|
|
5000
|
+
let r = k;
|
|
5001
|
+
while (parent.has(r) && parent.get(r) !== r)
|
|
5002
|
+
r = parent.get(r);
|
|
5003
|
+
return r;
|
|
5004
|
+
};
|
|
5005
|
+
const union = (a, b) => {
|
|
5006
|
+
if (!parent.has(a))
|
|
5007
|
+
parent.set(a, a);
|
|
5008
|
+
if (!parent.has(b))
|
|
5009
|
+
parent.set(b, b);
|
|
5010
|
+
const ra = find(a);
|
|
5011
|
+
const rb = find(b);
|
|
5012
|
+
if (ra !== rb)
|
|
5013
|
+
parent.set(ra, rb);
|
|
5014
|
+
};
|
|
5015
|
+
for (const w of wires)
|
|
5016
|
+
union(pointKey(w.x1, w.y1), pointKey(w.x2, w.y2));
|
|
5017
|
+
const boxes = new Map();
|
|
5018
|
+
const dbgSrc = new Map();
|
|
5019
|
+
const extend = (key, b, why = 'body') => {
|
|
5020
|
+
if (process.env['COPPERHEAD_DRAFT_TRACE'] === '1')
|
|
5021
|
+
dbgSrc.set(key, [...(dbgSrc.get(key) ?? []), `${why}[${b.minX.toFixed(0)},${b.minY.toFixed(0)}..${b.maxX.toFixed(0)},${b.maxY.toFixed(0)}]`]);
|
|
5022
|
+
const cur = boxes.get(key);
|
|
5023
|
+
boxes.set(key, cur ? { minX: Math.min(cur.minX, b.minX), minY: Math.min(cur.minY, b.minY), maxX: Math.max(cur.maxX, b.maxX), maxY: Math.max(cur.maxY, b.maxY) } : { ...b });
|
|
5024
|
+
};
|
|
5025
|
+
const compOwners = new Map();
|
|
5026
|
+
for (const [key, pl] of placed) {
|
|
5027
|
+
extend(key, pl.body);
|
|
5028
|
+
for (const pin of pl.sym.pins) {
|
|
5029
|
+
const p = pinAt(pl, pin);
|
|
5030
|
+
const pk = pointKey(p.x, p.y);
|
|
5031
|
+
if (!parent.has(pk))
|
|
5032
|
+
continue;
|
|
5033
|
+
const c = find(pk);
|
|
5034
|
+
compOwners.set(c, (compOwners.get(c) ?? new Set()).add(key));
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
const keyOf = new Map([...placed.entries()].map(([k, p]) => [p, k]));
|
|
5038
|
+
for (const { sym, pl } of emitPairs) {
|
|
5039
|
+
const key = keyOf.get(pl);
|
|
5040
|
+
if (!key)
|
|
5041
|
+
continue;
|
|
5042
|
+
extend(key, centeredTextBox(displayRefOf(pl), sym.refAt.x, sym.refAt.y), 'ref');
|
|
5043
|
+
extend(key, centeredTextBox(sym.value, sym.valueAt.x, sym.valueAt.y), 'value');
|
|
5044
|
+
}
|
|
5045
|
+
const compBox = new Map();
|
|
5046
|
+
const extendComp = (c, b) => {
|
|
5047
|
+
const cur = compBox.get(c);
|
|
5048
|
+
compBox.set(c, cur ? { minX: Math.min(cur.minX, b.minX), minY: Math.min(cur.minY, b.minY), maxX: Math.max(cur.maxX, b.maxX), maxY: Math.max(cur.maxY, b.maxY) } : { ...b });
|
|
5049
|
+
};
|
|
5050
|
+
for (const w of wires)
|
|
5051
|
+
extendComp(find(pointKey(w.x1, w.y1)), { minX: Math.min(w.x1, w.x2), minY: Math.min(w.y1, w.y2), maxX: Math.max(w.x1, w.x2), maxY: Math.max(w.y1, w.y2) });
|
|
5052
|
+
const compAt = (x, y) => {
|
|
5053
|
+
const pk = pointKey(x, y);
|
|
5054
|
+
if (parent.has(pk))
|
|
5055
|
+
return find(pk);
|
|
5056
|
+
const w = wires.find((s) => segContains(s, x, y));
|
|
5057
|
+
return w ? find(pointKey(w.x1, w.y1)) : null;
|
|
5058
|
+
};
|
|
5059
|
+
for (const l of labels) {
|
|
5060
|
+
const c = compAt(l.x, l.y);
|
|
5061
|
+
if (c)
|
|
5062
|
+
extendComp(c, labelTextBox(l.name, l.x, l.y, l.rot, l.kind));
|
|
5063
|
+
}
|
|
5064
|
+
for (const ps of extraSymbols) {
|
|
5065
|
+
const c = compAt(ps.at.x, ps.at.y);
|
|
5066
|
+
if (!c)
|
|
5067
|
+
continue;
|
|
5068
|
+
extendComp(c, { minX: ps.at.x - 2 * U, minY: ps.at.y - 2 * U, maxX: ps.at.x + 2 * U, maxY: ps.at.y + 2 * U });
|
|
5069
|
+
if (!ps.hideValue)
|
|
5070
|
+
extendComp(c, powerValueBox(ps.value, ps.valueAt.x, ps.valueAt.y));
|
|
5071
|
+
}
|
|
5072
|
+
const rootOf = (k) => {
|
|
5073
|
+
let r = k;
|
|
5074
|
+
for (let i = 0; i < 6 && boundTo.has(r); i++)
|
|
5075
|
+
r = boundTo.get(r);
|
|
5076
|
+
return r;
|
|
5077
|
+
};
|
|
5078
|
+
// A run with one owner cell (an IC and the parts hung on it) is that
|
|
5079
|
+
// cell's; a run shared by several independent cells (a bank's rail and
|
|
5080
|
+
// ground trunks, a cluster wire between two column parts) gives each
|
|
5081
|
+
// owner only the wires that touch its own pins, with the labels and
|
|
5082
|
+
// symbols at their far ends — attributed whole, a bank's trunk made every
|
|
5083
|
+
// capacitor's cell as wide as the bank.
|
|
5084
|
+
const pinComp = new Map(); // pin point -> owner key
|
|
5085
|
+
for (const [key, pl] of placed)
|
|
5086
|
+
for (const pin of pl.sym.pins) {
|
|
5087
|
+
const p = pinAt(pl, pin);
|
|
5088
|
+
pinComp.set(pointKey(p.x, p.y), key);
|
|
5089
|
+
}
|
|
5090
|
+
const itemsAt = new Map(); // point -> boxes of labels/symbols anchored there
|
|
5091
|
+
const addItem = (x, y, b) => {
|
|
5092
|
+
itemsAt.set(pointKey(x, y), [...(itemsAt.get(pointKey(x, y)) ?? []), b]);
|
|
5093
|
+
};
|
|
5094
|
+
for (const l of labels)
|
|
5095
|
+
addItem(l.x, l.y, labelTextBox(l.name, l.x, l.y, l.rot, l.kind));
|
|
5096
|
+
for (const ps of extraSymbols) {
|
|
5097
|
+
addItem(ps.at.x, ps.at.y, { minX: ps.at.x - 2 * U, minY: ps.at.y - 2 * U, maxX: ps.at.x + 2 * U, maxY: ps.at.y + 2 * U });
|
|
5098
|
+
if (!ps.hideValue)
|
|
5099
|
+
addItem(ps.at.x, ps.at.y, powerValueBox(ps.value, ps.valueAt.x, ps.valueAt.y));
|
|
5100
|
+
}
|
|
5101
|
+
for (const [c, owners] of compOwners) {
|
|
5102
|
+
const roots = new Set([...owners].map(rootOf));
|
|
5103
|
+
const b = compBox.get(c);
|
|
5104
|
+
if (!b)
|
|
5105
|
+
continue;
|
|
5106
|
+
if (roots.size === 1) {
|
|
5107
|
+
extend([...roots][0], b, `run(${[...owners].join('+')})`);
|
|
5108
|
+
continue;
|
|
5109
|
+
}
|
|
5110
|
+
for (const w of wires) {
|
|
5111
|
+
if (find(pointKey(w.x1, w.y1)) !== c)
|
|
5112
|
+
continue;
|
|
5113
|
+
const ends = [pointKey(w.x1, w.y1), pointKey(w.x2, w.y2)];
|
|
5114
|
+
const touching = new Set(ends.map((e) => pinComp.get(e)).filter((k) => k !== undefined).map(rootOf));
|
|
5115
|
+
for (const r of touching) {
|
|
5116
|
+
extend(r, { minX: Math.min(w.x1, w.x2), minY: Math.min(w.y1, w.y2), maxX: Math.max(w.x1, w.x2), maxY: Math.max(w.y1, w.y2) }, `wire(${w.net})`);
|
|
5117
|
+
for (const e of ends)
|
|
5118
|
+
for (const ib of itemsAt.get(e) ?? [])
|
|
5119
|
+
extend(r, ib, `item@${e}`);
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
for (const [key, b] of [...boxes.entries()]) {
|
|
5124
|
+
const r = rootOf(key);
|
|
5125
|
+
if (r !== key)
|
|
5126
|
+
extend(r, b, `child ${key}`);
|
|
5127
|
+
}
|
|
5128
|
+
for (const [key, pl] of placed) {
|
|
5129
|
+
if (rootOf(key) !== key)
|
|
5130
|
+
continue;
|
|
5131
|
+
const b = boxes.get(key);
|
|
5132
|
+
if (!b)
|
|
5133
|
+
continue;
|
|
5134
|
+
if (process.env['COPPERHEAD_DRAFT_TRACE'] && (b.maxX - b.minX > 4 * (pl.body.maxX - pl.body.minX) + 30 || b.maxY - b.minY > 4 * (pl.body.maxY - pl.body.minY) + 30)) {
|
|
5135
|
+
trace(`measured ${key}: drawn extent ${(b.maxX - b.minX).toFixed(0)}x${(b.maxY - b.minY).toFixed(0)} mm around a ${(pl.body.maxX - pl.body.minX).toFixed(0)}x${(pl.body.maxY - pl.body.minY).toFixed(0)} body <- ${(dbgSrc.get(key) ?? []).slice(0, 12).join(' ')}`);
|
|
5136
|
+
}
|
|
5137
|
+
measuredOut.set(key, {
|
|
5138
|
+
left: Math.max(0, pl.body.minX - b.minX),
|
|
5139
|
+
right: Math.max(0, b.maxX - pl.body.maxX),
|
|
5140
|
+
top: Math.max(0, pl.body.minY - b.minY),
|
|
5141
|
+
bottom: Math.max(0, b.maxY - pl.body.maxY),
|
|
5142
|
+
});
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
2235
5145
|
const allX = [...groupRects.map((r) => r.x1), ...groupRects.map((r) => r.x2)];
|
|
2236
5146
|
const allY = [...groupRects.map((r) => r.y1), ...groupRects.map((r) => r.y2)];
|
|
2237
5147
|
const contentW = allX.length ? Math.max(...allX) - Math.min(...allX) : 0;
|
|
@@ -2257,11 +5167,15 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2257
5167
|
// the frame, and an extent wider than the window keeps the centered offset
|
|
2258
5168
|
// (that overflow was already noted by the fit pass).
|
|
2259
5169
|
const textBoxes = [
|
|
2260
|
-
...labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot)),
|
|
5170
|
+
...labels.map((l) => labelTextBox(l.name, l.x, l.y, l.rot, l.kind)),
|
|
2261
5171
|
// power VALUE text is measured by the checker too, and the sweep above
|
|
2262
5172
|
// may have slid it past its group rect's margin (pic_programmer put a
|
|
2263
5173
|
// rail name 1 mm over the top edge of a compacted sheet)
|
|
2264
5174
|
...extraSymbols.filter((s) => !s.hideValue).map((s) => powerValueBox(s.value, s.valueAt.x, s.valueAt.y)),
|
|
5175
|
+
// group captions: left-top justified at CAPTION_SIZE from the box
|
|
5176
|
+
// corner, the way emit.ts writes them and the checker measures them; a
|
|
5177
|
+
// long caption on a narrow group can outrun the box's right edge
|
|
5178
|
+
...groupRects.map((r) => ({ minX: r.x1 + 2, minY: r.y1 + 2, maxX: r.x1 + 2 + Math.max(1, r.name.length) * LABEL_ADVANCE * CAPTION_SIZE, maxY: r.y1 + 2 + CAPTION_SIZE })),
|
|
2265
5179
|
];
|
|
2266
5180
|
const fullMinX = Math.min(minX, ...textBoxes.map((b) => b.minX));
|
|
2267
5181
|
const fullMaxX = Math.max(minX + contentW, ...textBoxes.map((b) => b.maxX));
|
|
@@ -2286,7 +5200,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2286
5200
|
// the bottom edge is the engine's own usable bottom, ABOVE the title strip:
|
|
2287
5201
|
// content that fills the sheet's height exactly would otherwise carry the
|
|
2288
5202
|
// centering pass's 4-unit downward offset into the reserved corner
|
|
2289
|
-
dy = clampShift(dy, fullMinY, fullMaxY, FRAME, paper.h - FRAME - TITLE_STRIP);
|
|
5203
|
+
dy = clampShift(dy, fullMinY, fullMaxY, FRAME, fit.intoStrip ? paper.h - FRAME : paper.h - FRAME - TITLE_STRIP);
|
|
2290
5204
|
const shift = (o) => {
|
|
2291
5205
|
if (o.x !== undefined)
|
|
2292
5206
|
o.x += dx;
|
|
@@ -2362,6 +5276,7 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2362
5276
|
noConnects,
|
|
2363
5277
|
rectangles: groupRects.map((r) => ({ x1: r.x1, y1: r.y1, x2: r.x2, y2: r.y2, stroke: 'solid', name: r.name })),
|
|
2364
5278
|
captions: groupRects.map((r) => ({ text: r.name, x: r.x1 + 2, y: r.y1 + 2, name: r.name })),
|
|
5279
|
+
netColors: netColorsOf(intent.nets, netClasses),
|
|
2365
5280
|
};
|
|
2366
5281
|
const report = {
|
|
2367
5282
|
groups: groupNames.map((g) => ({
|
|
@@ -2378,12 +5293,18 @@ export function draftSchematicPlacement(validated, projectName, today) {
|
|
|
2378
5293
|
labelCount: labels.length,
|
|
2379
5294
|
pwrFlags,
|
|
2380
5295
|
noConnects: noConnects.length,
|
|
5296
|
+
sheetFit: {
|
|
5297
|
+
paper: paper.name,
|
|
5298
|
+
inkUtilization: [...placed.values()].reduce((a, p) => a + p.cellW * p.cellH * U * U, 0) / (usableW(paper) * usableH(paper)),
|
|
5299
|
+
compaction,
|
|
5300
|
+
misses: [...budgetMisses],
|
|
5301
|
+
},
|
|
2381
5302
|
paper: paper.name,
|
|
2382
5303
|
notes,
|
|
2383
5304
|
mergedNets,
|
|
2384
5305
|
labelOverlaps,
|
|
2385
5306
|
labelOverlapBudgetExceeded,
|
|
2386
5307
|
};
|
|
2387
|
-
return { model, report };
|
|
5308
|
+
return { model, report, rects: groupRects, measured: measuredOut, reach: reachOut, wrapped: fit.wrap !== null };
|
|
2388
5309
|
}
|
|
2389
5310
|
//# sourceMappingURL=engine.js.map
|