@tscircuit/copper-pour-solver 0.0.32 → 0.0.34
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 +139 -20
- package/cosmos.config.json +2 -1
- package/dist/index.d.ts +24 -6
- package/dist/index.js +176 -7
- package/lib/circuit-json/ConvertCircuitJsonToInputProblemOptions.ts +18 -0
- package/lib/circuit-json/buildSubcircuitConnectivityLookup.ts +54 -0
- package/lib/circuit-json/convert-circuit-json-to-input-problem.ts +31 -21
- package/lib/circuit-json/getElementSubcircuitConnectivityKey.ts +8 -0
- package/lib/circuit-json/resolvePourConnectivityKey.ts +61 -0
- package/lib/index.ts +1 -0
- package/lib/solvers/copper-pour/polygon-primitives.ts +53 -0
- package/lib/solvers/copper-pour/process-obstacles.ts +17 -0
- package/lib/types.ts +11 -0
- package/package.json +5 -1
- package/site/Welcome.page.tsx +214 -0
- package/tests/__snapshots__/pill-pad-copper-pour.snap.svg +1 -0
- package/tests/connectivity-key-api.test.ts +100 -0
- package/tests/pill-pad-copper-pour.test.ts +124 -0
- package/tests/repro01-business-via-card/__snapshots__/repro01-business-via-card.snap.svg +1 -1
- package/tests/utils/run-solver-and-render-to-svg.ts +5 -13
- package/vercel.json +5 -0
package/README.md
CHANGED
|
@@ -1,37 +1,156 @@
|
|
|
1
1
|
# @tscircuit/copper-pour-solver
|
|
2
2
|
|
|
3
|
-
Solves
|
|
3
|
+
Solves PCB copper pour regions from Circuit JSON or from a small geometry input
|
|
4
|
+
format, returning `pcb_copper_pour`-ready B-Rep shapes.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
import { CopperPourPipelineSolver } from "@tscircuit/copper-pour-solver"
|
|
6
|
+
## Install
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
```bash
|
|
9
|
+
bun add @tscircuit/copper-pour-solver
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This package expects TypeScript 5 as a peer dependency.
|
|
13
|
+
|
|
14
|
+
## Basic Usage With Circuit JSON
|
|
15
|
+
|
|
16
|
+
Initialize the geometry runtime once before solving. Then convert Circuit JSON
|
|
17
|
+
into the solver input format, run the solver, and map the returned B-Rep shapes
|
|
18
|
+
back into `pcb_copper_pour` elements.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
CopperPourPipelineSolver,
|
|
23
|
+
convertCircuitJsonToInputProblem,
|
|
24
|
+
initializeManifoldGeometry,
|
|
25
|
+
} from "@tscircuit/copper-pour-solver"
|
|
26
|
+
|
|
27
|
+
await initializeManifoldGeometry()
|
|
28
|
+
|
|
29
|
+
const inputProblem = convertCircuitJsonToInputProblem(circuitJson, {
|
|
30
|
+
layer: "top",
|
|
31
|
+
source_net_name: "GND",
|
|
32
|
+
pad_margin: 0.4,
|
|
33
|
+
trace_margin: 0.2,
|
|
34
|
+
board_edge_margin: 0.1,
|
|
35
|
+
cutout_margin: 0.2,
|
|
11
36
|
})
|
|
12
37
|
|
|
13
|
-
solver
|
|
38
|
+
const solver = new CopperPourPipelineSolver(inputProblem)
|
|
39
|
+
const { brep_shapes } = solver.getOutput()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`convertCircuitJsonToInputProblem` reads board bounds or outline, SMT pads,
|
|
43
|
+
plated holes, mechanical holes, vias, traces, and cutouts for the selected layer.
|
|
44
|
+
Pads and traces connected to the selected source net are kept connected to the
|
|
45
|
+
pour; unrelated geometry is subtracted using the configured margins.
|
|
46
|
+
|
|
47
|
+
## Selecting The Pour Net
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
Prefer selecting by source net name or id:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const inputProblem = convertCircuitJsonToInputProblem(circuitJson, {
|
|
53
|
+
layer: "top",
|
|
54
|
+
source_net_name: "GND",
|
|
55
|
+
pad_margin: 0.4,
|
|
56
|
+
trace_margin: 0.2,
|
|
57
|
+
})
|
|
17
58
|
```
|
|
18
59
|
|
|
19
|
-
|
|
60
|
+
You can also pass the source net's stable `subcircuit_connectivity_map_key`
|
|
61
|
+
directly:
|
|
20
62
|
|
|
21
|
-
|
|
63
|
+
```ts
|
|
64
|
+
const gnd = circuitJson.find(
|
|
65
|
+
(element) => element.type === "source_net" && element.name === "GND",
|
|
66
|
+
)
|
|
22
67
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
68
|
+
const inputProblem = convertCircuitJsonToInputProblem(circuitJson, {
|
|
69
|
+
layer: "top",
|
|
70
|
+
subcircuit_connectivity_map_key: gnd.subcircuit_connectivity_map_key,
|
|
71
|
+
pad_margin: 0.4,
|
|
72
|
+
trace_margin: 0.2,
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Do not generate or pass ids from `circuit-json-to-connectivity-map`. The
|
|
77
|
+
converter handles PCB connectivity internally and normalizes it to stable
|
|
78
|
+
`subcircuit_connectivity_map_key` values.
|
|
79
|
+
|
|
80
|
+
## Manual Input
|
|
81
|
+
|
|
82
|
+
You can skip Circuit JSON conversion and provide the solver input directly.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
CopperPourPipelineSolver,
|
|
87
|
+
initializeManifoldGeometry,
|
|
88
|
+
type InputProblem,
|
|
89
|
+
} from "@tscircuit/copper-pour-solver"
|
|
90
|
+
|
|
91
|
+
await initializeManifoldGeometry()
|
|
92
|
+
|
|
93
|
+
const input: InputProblem = {
|
|
94
|
+
regionsForPour: [
|
|
95
|
+
{
|
|
96
|
+
shape: "rect",
|
|
97
|
+
layer: "top",
|
|
98
|
+
bounds: { minX: -10, minY: -5, maxX: 10, maxY: 5 },
|
|
99
|
+
connectivityKey: "net:GND",
|
|
100
|
+
padMargin: 0.4,
|
|
101
|
+
traceMargin: 0.2,
|
|
102
|
+
board_edge_margin: 0.1,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
pads: [
|
|
106
|
+
{
|
|
107
|
+
shape: "circle",
|
|
108
|
+
padId: "via_1",
|
|
109
|
+
layer: "top",
|
|
110
|
+
connectivityKey: "net:VCC",
|
|
111
|
+
x: 0,
|
|
112
|
+
y: 0,
|
|
113
|
+
radius: 0.5,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
27
116
|
}
|
|
28
117
|
|
|
29
|
-
|
|
30
|
-
|
|
118
|
+
const output = new CopperPourPipelineSolver(input).getOutput()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Supported input pad shapes are `rect`, `circle`, `pill`, `trace`, and `polygon`.
|
|
122
|
+
Use the same `connectivityKey` as the pour for pads/traces that should connect to
|
|
123
|
+
the copper island; use a different key for blockers that should be cleared.
|
|
124
|
+
|
|
125
|
+
## Output
|
|
126
|
+
|
|
127
|
+
`getOutput()` returns:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
interface PipelineOutput {
|
|
131
|
+
brep_shapes: BRepShape[]
|
|
31
132
|
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each B-Rep shape is compatible with Circuit JSON copper pour data:
|
|
32
136
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
137
|
+
```ts
|
|
138
|
+
interface BRepShape {
|
|
139
|
+
outer_ring: {
|
|
140
|
+
vertices: Array<{ x: number; y: number; bulge?: number }>
|
|
141
|
+
}
|
|
142
|
+
inner_rings: Array<{
|
|
143
|
+
vertices: Array<{ x: number; y: number; bulge?: number }>
|
|
144
|
+
}>
|
|
36
145
|
}
|
|
37
146
|
```
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
bun install
|
|
152
|
+
bun run build
|
|
153
|
+
bun test
|
|
154
|
+
bun start
|
|
155
|
+
bun run build:site
|
|
156
|
+
```
|
package/cosmos.config.json
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BasePipelineSolver } from '@tscircuit/solver-utils';
|
|
2
2
|
import { Bounds, Point } from '@tscircuit/math-utils';
|
|
3
|
-
import { BRepShape,
|
|
3
|
+
import { BRepShape, LayerRef, Point as Point$1, AnyCircuitElement } from 'circuit-json';
|
|
4
4
|
|
|
5
5
|
interface InputPourRegion {
|
|
6
6
|
shape: "rect";
|
|
@@ -28,6 +28,15 @@ interface InputCircularPad extends BaseInputPad {
|
|
|
28
28
|
y: number;
|
|
29
29
|
radius: number;
|
|
30
30
|
}
|
|
31
|
+
interface InputPillPad extends BaseInputPad {
|
|
32
|
+
shape: "pill";
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
radius: number;
|
|
38
|
+
ccwRotation: number;
|
|
39
|
+
}
|
|
31
40
|
interface InputTracePad extends BaseInputPad {
|
|
32
41
|
shape: "trace";
|
|
33
42
|
width: number;
|
|
@@ -37,7 +46,7 @@ interface InputPolygonPad extends BaseInputPad {
|
|
|
37
46
|
shape: "polygon";
|
|
38
47
|
points: Point[];
|
|
39
48
|
}
|
|
40
|
-
type InputPad = InputRectPad | InputCircularPad | InputTracePad | InputPolygonPad;
|
|
49
|
+
type InputPad = InputRectPad | InputCircularPad | InputPillPad | InputTracePad | InputPolygonPad;
|
|
41
50
|
interface InputProblem {
|
|
42
51
|
regionsForPour: InputPourRegion[];
|
|
43
52
|
pads: InputPad[];
|
|
@@ -56,14 +65,23 @@ declare class CopperPourPipelineSolver extends BasePipelineSolver<InputProblem>
|
|
|
56
65
|
|
|
57
66
|
declare const initializeManifoldGeometry: () => Promise<void>;
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
interface ConvertCircuitJsonToInputProblemOptions {
|
|
60
69
|
layer: LayerRef;
|
|
61
|
-
|
|
70
|
+
source_net_id?: string;
|
|
71
|
+
source_net_name?: string;
|
|
72
|
+
subcircuit_connectivity_map_key?: string;
|
|
73
|
+
/**
|
|
74
|
+
* @deprecated Use subcircuit_connectivity_map_key, source_net_id, or
|
|
75
|
+
* source_net_name. Generated connectivity-map ids are intentionally rejected.
|
|
76
|
+
*/
|
|
77
|
+
pour_connectivity_key?: string;
|
|
62
78
|
pad_margin: number;
|
|
63
79
|
trace_margin: number;
|
|
64
80
|
board_edge_margin?: number;
|
|
65
81
|
cutout_margin?: number;
|
|
66
82
|
outline?: Point$1[];
|
|
67
|
-
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
declare const convertCircuitJsonToInputProblem: (circuitJson: AnyCircuitElement[], options: ConvertCircuitJsonToInputProblemOptions) => InputProblem;
|
|
68
86
|
|
|
69
|
-
export { type BaseInputPad, CopperPourPipelineSolver, type InputCircularPad, type InputPad, type InputPolygonPad, type InputPourRegion, type InputProblem, type InputRectPad, type InputTracePad, type PipelineOutput, convertCircuitJsonToInputProblem, initializeManifoldGeometry };
|
|
87
|
+
export { type BaseInputPad, type ConvertCircuitJsonToInputProblemOptions, CopperPourPipelineSolver, type InputCircularPad, type InputPad, type InputPillPad, type InputPolygonPad, type InputPourRegion, type InputProblem, type InputRectPad, type InputTracePad, type PipelineOutput, convertCircuitJsonToInputProblem, initializeManifoldGeometry };
|
package/dist/index.js
CHANGED
|
@@ -321,11 +321,50 @@ var segmentToPolygon = (start, end, width) => {
|
|
|
321
321
|
y: centerY + point.x * sinAngle + point.y * cosAngle
|
|
322
322
|
}));
|
|
323
323
|
};
|
|
324
|
+
var pillToPolygon = (center, width, height, radius, ccwRotation = 0, numSegments = 32) => {
|
|
325
|
+
if (radius <= 0) return [];
|
|
326
|
+
const isVertical = height >= width;
|
|
327
|
+
const centerlineLength = Math.max(width, height) - 2 * radius;
|
|
328
|
+
const halfCenterline = Math.max(0, centerlineLength / 2);
|
|
329
|
+
const baseAngle = isVertical ? Math.PI / 2 : 0;
|
|
330
|
+
const rotation = baseAngle + ccwRotation * Math.PI / 180;
|
|
331
|
+
const cosAngle = Math.cos(rotation);
|
|
332
|
+
const sinAngle = Math.sin(rotation);
|
|
333
|
+
const start = {
|
|
334
|
+
x: center.x - halfCenterline * cosAngle,
|
|
335
|
+
y: center.y - halfCenterline * sinAngle
|
|
336
|
+
};
|
|
337
|
+
const end = {
|
|
338
|
+
x: center.x + halfCenterline * cosAngle,
|
|
339
|
+
y: center.y + halfCenterline * sinAngle
|
|
340
|
+
};
|
|
341
|
+
if (halfCenterline === 0) {
|
|
342
|
+
return circleToPolygon(center, radius, numSegments);
|
|
343
|
+
}
|
|
344
|
+
const points = [];
|
|
345
|
+
const halfSegments = Math.max(4, Math.floor(numSegments / 2));
|
|
346
|
+
for (let i = 0; i <= halfSegments; i++) {
|
|
347
|
+
const angle = rotation - Math.PI / 2 + i / halfSegments * Math.PI;
|
|
348
|
+
points.push({
|
|
349
|
+
x: end.x + radius * Math.cos(angle),
|
|
350
|
+
y: end.y + radius * Math.sin(angle)
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
for (let i = 0; i <= halfSegments; i++) {
|
|
354
|
+
const angle = rotation + Math.PI / 2 + i / halfSegments * Math.PI;
|
|
355
|
+
points.push({
|
|
356
|
+
x: start.x + radius * Math.cos(angle),
|
|
357
|
+
y: start.y + radius * Math.sin(angle)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return points;
|
|
361
|
+
};
|
|
324
362
|
|
|
325
363
|
// lib/solvers/copper-pour/process-obstacles.ts
|
|
326
364
|
var isRectPad = (pad) => pad.shape === "rect";
|
|
327
365
|
var isTracePad = (pad) => pad.shape === "trace";
|
|
328
366
|
var isCircularPad = (pad) => pad.shape === "circle";
|
|
367
|
+
var isPillPad = (pad) => pad.shape === "pill";
|
|
329
368
|
var isPolygonPad = (pad) => pad.shape === "polygon";
|
|
330
369
|
var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline) => {
|
|
331
370
|
const polygonsToSubtract = [];
|
|
@@ -378,6 +417,19 @@ var processObstaclesForPour = (pads, pourConnectivityKey, margins, boardOutline)
|
|
|
378
417
|
);
|
|
379
418
|
continue;
|
|
380
419
|
}
|
|
420
|
+
if (isPillPad(pad)) {
|
|
421
|
+
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
422
|
+
polygonsToSubtract.push(
|
|
423
|
+
pillToPolygon(
|
|
424
|
+
{ x: pad.x, y: pad.y },
|
|
425
|
+
pad.width + margin * 2,
|
|
426
|
+
pad.height + margin * 2,
|
|
427
|
+
pad.radius + margin,
|
|
428
|
+
pad.ccwRotation
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
381
433
|
if (isRectPad(pad)) {
|
|
382
434
|
const margin = isHoleOrCutout ? cutoutMargin ?? 0 : padMargin;
|
|
383
435
|
const { bounds } = pad;
|
|
@@ -485,19 +537,123 @@ var CopperPourPipelineSolver = class extends BasePipelineSolver {
|
|
|
485
537
|
|
|
486
538
|
// lib/circuit-json/convert-circuit-json-to-input-problem.ts
|
|
487
539
|
import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map";
|
|
540
|
+
|
|
541
|
+
// lib/circuit-json/buildSubcircuitConnectivityLookup.ts
|
|
542
|
+
import { getElementId } from "@tscircuit/circuit-json-util";
|
|
543
|
+
|
|
544
|
+
// lib/circuit-json/getElementSubcircuitConnectivityKey.ts
|
|
545
|
+
var getElementSubcircuitConnectivityKey = (element) => {
|
|
546
|
+
const key = element.subcircuit_connectivity_map_key;
|
|
547
|
+
return typeof key === "string" && key.length > 0 ? key : void 0;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// lib/circuit-json/buildSubcircuitConnectivityLookup.ts
|
|
551
|
+
var buildSubcircuitConnectivityLookup = (circuitJson, connectivityMap) => {
|
|
552
|
+
const idToSubcircuitConnectivityKey = {};
|
|
553
|
+
for (const element of circuitJson) {
|
|
554
|
+
const id = getElementId(element);
|
|
555
|
+
const key = getElementSubcircuitConnectivityKey(element);
|
|
556
|
+
if (id && key) {
|
|
557
|
+
idToSubcircuitConnectivityKey[id] = key;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const generatedNetIdToSubcircuitConnectivityKey = {};
|
|
561
|
+
for (const [generatedNetId, connectedIds] of Object.entries(
|
|
562
|
+
connectivityMap.netMap
|
|
563
|
+
)) {
|
|
564
|
+
const connectedSubcircuitKeys = new Set(
|
|
565
|
+
connectedIds.map((id) => idToSubcircuitConnectivityKey[id]).filter((key) => Boolean(key))
|
|
566
|
+
);
|
|
567
|
+
if (connectedSubcircuitKeys.size > 1) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
`Multiple subcircuit connectivity keys found for generated connectivity map net "${generatedNetId}": ${Array.from(connectedSubcircuitKeys).join(", ")}`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const subcircuitKey = connectedSubcircuitKeys.values().next().value;
|
|
573
|
+
if (subcircuitKey) {
|
|
574
|
+
generatedNetIdToSubcircuitConnectivityKey[generatedNetId] = subcircuitKey;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
knownSubcircuitConnectivityKeys: new Set(
|
|
579
|
+
Object.values(idToSubcircuitConnectivityKey)
|
|
580
|
+
),
|
|
581
|
+
getSubcircuitConnectivityKeyForId(id) {
|
|
582
|
+
const directKey = idToSubcircuitConnectivityKey[id];
|
|
583
|
+
if (directKey) return directKey;
|
|
584
|
+
const generatedNetId = connectivityMap.getNetConnectedToId(id);
|
|
585
|
+
if (!generatedNetId) return void 0;
|
|
586
|
+
return generatedNetIdToSubcircuitConnectivityKey[generatedNetId];
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// lib/circuit-json/resolvePourConnectivityKey.ts
|
|
592
|
+
var resolvePourConnectivityKey = (circuitJson, options, knownSubcircuitConnectivityKeys) => {
|
|
593
|
+
if (options.subcircuit_connectivity_map_key) {
|
|
594
|
+
return options.subcircuit_connectivity_map_key;
|
|
595
|
+
}
|
|
596
|
+
if (options.source_net_id) {
|
|
597
|
+
const sourceNet = circuitJson.find(
|
|
598
|
+
(element) => element.type === "source_net" && element.source_net_id === options.source_net_id
|
|
599
|
+
);
|
|
600
|
+
if (!sourceNet) {
|
|
601
|
+
throw new Error(`No source_net found with id "${options.source_net_id}"`);
|
|
602
|
+
}
|
|
603
|
+
if (!sourceNet.subcircuit_connectivity_map_key) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`source_net "${options.source_net_id}" has no subcircuit_connectivity_map_key`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return sourceNet.subcircuit_connectivity_map_key;
|
|
609
|
+
}
|
|
610
|
+
if (options.source_net_name) {
|
|
611
|
+
const sourceNet = circuitJson.find(
|
|
612
|
+
(element) => element.type === "source_net" && element.name === options.source_net_name
|
|
613
|
+
);
|
|
614
|
+
if (!sourceNet) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`No source_net found with name "${options.source_net_name}"`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
if (!sourceNet.subcircuit_connectivity_map_key) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`source_net "${options.source_net_name}" has no subcircuit_connectivity_map_key`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return sourceNet.subcircuit_connectivity_map_key;
|
|
625
|
+
}
|
|
626
|
+
if (options.pour_connectivity_key) {
|
|
627
|
+
if (!knownSubcircuitConnectivityKeys.has(options.pour_connectivity_key)) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`pour_connectivity_key must be a subcircuit_connectivity_map_key. Use subcircuit_connectivity_map_key, source_net_id, or source_net_name instead of a generated connectivity-map id.`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
return options.pour_connectivity_key;
|
|
633
|
+
}
|
|
634
|
+
throw new Error(
|
|
635
|
+
"Copper pour requires source_net_id, source_net_name, or subcircuit_connectivity_map_key"
|
|
636
|
+
);
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// lib/circuit-json/convert-circuit-json-to-input-problem.ts
|
|
488
640
|
var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
489
641
|
const pcb_board = circuitJson.find((e) => e.type === "pcb_board");
|
|
490
642
|
if (!pcb_board) throw new Error("No pcb_board found in circuit json");
|
|
491
643
|
const connectivityMap = getFullConnectivityMapFromCircuitJson(circuitJson);
|
|
644
|
+
const { knownSubcircuitConnectivityKeys, getSubcircuitConnectivityKeyForId } = buildSubcircuitConnectivityLookup(circuitJson, connectivityMap);
|
|
645
|
+
const pourConnectivityKey = resolvePourConnectivityKey(
|
|
646
|
+
circuitJson,
|
|
647
|
+
options,
|
|
648
|
+
knownSubcircuitConnectivityKeys
|
|
649
|
+
);
|
|
492
650
|
const pads = [];
|
|
493
651
|
for (const elm of circuitJson) {
|
|
494
652
|
if (elm.type === "pcb_smtpad") {
|
|
495
653
|
const smtpad = elm;
|
|
496
654
|
if (smtpad.layer !== options.layer) continue;
|
|
497
655
|
let connectivityKey;
|
|
498
|
-
connectivityKey =
|
|
499
|
-
smtpad.pcb_smtpad_id
|
|
500
|
-
);
|
|
656
|
+
connectivityKey = getSubcircuitConnectivityKeyForId(smtpad.pcb_smtpad_id);
|
|
501
657
|
if (!connectivityKey) {
|
|
502
658
|
connectivityKey = `unconnected:${smtpad.pcb_smtpad_id}`;
|
|
503
659
|
}
|
|
@@ -524,11 +680,24 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
524
680
|
y: smtpad.y,
|
|
525
681
|
radius: smtpad.radius
|
|
526
682
|
});
|
|
683
|
+
} else if (smtpad.shape === "pill" || smtpad.shape === "rotated_pill") {
|
|
684
|
+
pads.push({
|
|
685
|
+
shape: "pill",
|
|
686
|
+
padId: smtpad.pcb_smtpad_id,
|
|
687
|
+
layer: smtpad.layer,
|
|
688
|
+
connectivityKey,
|
|
689
|
+
x: smtpad.x,
|
|
690
|
+
y: smtpad.y,
|
|
691
|
+
width: smtpad.width,
|
|
692
|
+
height: smtpad.height,
|
|
693
|
+
radius: smtpad.radius,
|
|
694
|
+
ccwRotation: smtpad.shape === "rotated_pill" ? smtpad.ccw_rotation : 0
|
|
695
|
+
});
|
|
527
696
|
}
|
|
528
697
|
} else if (elm.type === "pcb_plated_hole") {
|
|
529
698
|
const platedHole = elm;
|
|
530
699
|
if (!platedHole.layers.includes(options.layer)) continue;
|
|
531
|
-
let connectivityKey =
|
|
700
|
+
let connectivityKey = getSubcircuitConnectivityKeyForId(
|
|
532
701
|
platedHole.pcb_plated_hole_id
|
|
533
702
|
);
|
|
534
703
|
if (!connectivityKey) {
|
|
@@ -616,7 +785,7 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
616
785
|
} else if (elm.type === "pcb_via") {
|
|
617
786
|
const via = elm;
|
|
618
787
|
if (!via.layers.includes(options.layer)) continue;
|
|
619
|
-
const connectivityKey =
|
|
788
|
+
const connectivityKey = getSubcircuitConnectivityKeyForId(via.pcb_via_id) ?? `unconnected-via:${via.pcb_via_id}`;
|
|
620
789
|
pads.push({
|
|
621
790
|
shape: "circle",
|
|
622
791
|
padId: via.pcb_via_id,
|
|
@@ -628,7 +797,7 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
628
797
|
});
|
|
629
798
|
} else if (elm.type === "pcb_trace") {
|
|
630
799
|
const trace = elm;
|
|
631
|
-
const connectivityKey =
|
|
800
|
+
const connectivityKey = getSubcircuitConnectivityKeyForId(
|
|
632
801
|
trace.pcb_trace_id
|
|
633
802
|
);
|
|
634
803
|
if (!connectivityKey) continue;
|
|
@@ -687,7 +856,7 @@ var convertCircuitJsonToInputProblem = (circuitJson, options) => {
|
|
|
687
856
|
layer: options.layer,
|
|
688
857
|
bounds,
|
|
689
858
|
outline,
|
|
690
|
-
connectivityKey:
|
|
859
|
+
connectivityKey: pourConnectivityKey,
|
|
691
860
|
padMargin: options.pad_margin,
|
|
692
861
|
traceMargin: options.trace_margin,
|
|
693
862
|
board_edge_margin: options.board_edge_margin ?? 0,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LayerRef, Point } from "circuit-json"
|
|
2
|
+
|
|
3
|
+
export interface ConvertCircuitJsonToInputProblemOptions {
|
|
4
|
+
layer: LayerRef
|
|
5
|
+
source_net_id?: string
|
|
6
|
+
source_net_name?: string
|
|
7
|
+
subcircuit_connectivity_map_key?: string
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use subcircuit_connectivity_map_key, source_net_id, or
|
|
10
|
+
* source_net_name. Generated connectivity-map ids are intentionally rejected.
|
|
11
|
+
*/
|
|
12
|
+
pour_connectivity_key?: string
|
|
13
|
+
pad_margin: number
|
|
14
|
+
trace_margin: number
|
|
15
|
+
board_edge_margin?: number
|
|
16
|
+
cutout_margin?: number
|
|
17
|
+
outline?: Point[]
|
|
18
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AnyCircuitElement } from "circuit-json"
|
|
2
|
+
import type { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map"
|
|
3
|
+
import { getElementId } from "@tscircuit/circuit-json-util"
|
|
4
|
+
import { getElementSubcircuitConnectivityKey } from "./getElementSubcircuitConnectivityKey"
|
|
5
|
+
|
|
6
|
+
export const buildSubcircuitConnectivityLookup = (
|
|
7
|
+
circuitJson: AnyCircuitElement[],
|
|
8
|
+
connectivityMap: ReturnType<typeof getFullConnectivityMapFromCircuitJson>,
|
|
9
|
+
) => {
|
|
10
|
+
const idToSubcircuitConnectivityKey: Record<string, string> = {}
|
|
11
|
+
|
|
12
|
+
for (const element of circuitJson) {
|
|
13
|
+
const id = getElementId(element)
|
|
14
|
+
const key = getElementSubcircuitConnectivityKey(element)
|
|
15
|
+
if (id && key) {
|
|
16
|
+
idToSubcircuitConnectivityKey[id] = key
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const generatedNetIdToSubcircuitConnectivityKey: Record<string, string> = {}
|
|
21
|
+
for (const [generatedNetId, connectedIds] of Object.entries(
|
|
22
|
+
connectivityMap.netMap,
|
|
23
|
+
)) {
|
|
24
|
+
const connectedSubcircuitKeys = new Set(
|
|
25
|
+
connectedIds
|
|
26
|
+
.map((id) => idToSubcircuitConnectivityKey[id])
|
|
27
|
+
.filter((key): key is string => Boolean(key)),
|
|
28
|
+
)
|
|
29
|
+
if (connectedSubcircuitKeys.size > 1) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Multiple subcircuit connectivity keys found for generated connectivity map net "${generatedNetId}": ${Array.from(connectedSubcircuitKeys).join(", ")}`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
const subcircuitKey = connectedSubcircuitKeys.values().next().value
|
|
35
|
+
if (subcircuitKey) {
|
|
36
|
+
generatedNetIdToSubcircuitConnectivityKey[generatedNetId] = subcircuitKey
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
knownSubcircuitConnectivityKeys: new Set(
|
|
42
|
+
Object.values(idToSubcircuitConnectivityKey),
|
|
43
|
+
),
|
|
44
|
+
getSubcircuitConnectivityKeyForId(id: string): string | undefined {
|
|
45
|
+
const directKey = idToSubcircuitConnectivityKey[id]
|
|
46
|
+
if (directKey) return directKey
|
|
47
|
+
|
|
48
|
+
const generatedNetId = connectivityMap.getNetConnectedToId(id)
|
|
49
|
+
if (!generatedNetId) return undefined
|
|
50
|
+
|
|
51
|
+
return generatedNetIdToSubcircuitConnectivityKey[generatedNetId]
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,39 +1,30 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnyCircuitElement,
|
|
3
|
-
LayerRef,
|
|
4
3
|
PcbBoard,
|
|
5
4
|
PcbHole,
|
|
6
5
|
PcbPlatedHole,
|
|
7
|
-
PcbPort,
|
|
8
6
|
PcbSmtPad,
|
|
9
7
|
PcbTrace,
|
|
10
8
|
PcbVia,
|
|
11
9
|
Point,
|
|
12
|
-
SourceNet,
|
|
13
|
-
SourcePort,
|
|
14
|
-
SourceTrace,
|
|
15
10
|
} from "circuit-json"
|
|
16
11
|
import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map"
|
|
17
12
|
import type {
|
|
18
13
|
InputCircularPad,
|
|
19
14
|
InputPad,
|
|
15
|
+
InputPillPad,
|
|
20
16
|
InputPolygonPad,
|
|
21
17
|
InputProblem,
|
|
22
18
|
InputRectPad,
|
|
23
19
|
InputTracePad,
|
|
24
20
|
} from "lib/types"
|
|
21
|
+
import type { ConvertCircuitJsonToInputProblemOptions } from "./ConvertCircuitJsonToInputProblemOptions"
|
|
22
|
+
import { buildSubcircuitConnectivityLookup } from "./buildSubcircuitConnectivityLookup"
|
|
23
|
+
import { resolvePourConnectivityKey } from "./resolvePourConnectivityKey"
|
|
25
24
|
|
|
26
25
|
export const convertCircuitJsonToInputProblem = (
|
|
27
26
|
circuitJson: AnyCircuitElement[],
|
|
28
|
-
options:
|
|
29
|
-
layer: LayerRef
|
|
30
|
-
pour_connectivity_key: string
|
|
31
|
-
pad_margin: number
|
|
32
|
-
trace_margin: number
|
|
33
|
-
board_edge_margin?: number
|
|
34
|
-
cutout_margin?: number
|
|
35
|
-
outline?: Point[]
|
|
36
|
-
},
|
|
27
|
+
options: ConvertCircuitJsonToInputProblemOptions,
|
|
37
28
|
): InputProblem => {
|
|
38
29
|
const pcb_board = circuitJson.find((e) => e.type === "pcb_board") as
|
|
39
30
|
| PcbBoard
|
|
@@ -42,6 +33,13 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
42
33
|
if (!pcb_board) throw new Error("No pcb_board found in circuit json")
|
|
43
34
|
|
|
44
35
|
const connectivityMap = getFullConnectivityMapFromCircuitJson(circuitJson)
|
|
36
|
+
const { knownSubcircuitConnectivityKeys, getSubcircuitConnectivityKeyForId } =
|
|
37
|
+
buildSubcircuitConnectivityLookup(circuitJson, connectivityMap)
|
|
38
|
+
const pourConnectivityKey = resolvePourConnectivityKey(
|
|
39
|
+
circuitJson,
|
|
40
|
+
options,
|
|
41
|
+
knownSubcircuitConnectivityKeys,
|
|
42
|
+
)
|
|
45
43
|
|
|
46
44
|
const pads: InputPad[] = []
|
|
47
45
|
|
|
@@ -51,9 +49,7 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
51
49
|
if (smtpad.layer !== options.layer) continue
|
|
52
50
|
|
|
53
51
|
let connectivityKey: string | undefined
|
|
54
|
-
connectivityKey =
|
|
55
|
-
smtpad.pcb_smtpad_id,
|
|
56
|
-
)
|
|
52
|
+
connectivityKey = getSubcircuitConnectivityKeyForId(smtpad.pcb_smtpad_id)
|
|
57
53
|
if (!connectivityKey) {
|
|
58
54
|
connectivityKey = `unconnected:${smtpad.pcb_smtpad_id}`
|
|
59
55
|
}
|
|
@@ -81,12 +77,26 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
81
77
|
y: smtpad.y,
|
|
82
78
|
radius: smtpad.radius!,
|
|
83
79
|
} as InputCircularPad)
|
|
80
|
+
} else if (smtpad.shape === "pill" || smtpad.shape === "rotated_pill") {
|
|
81
|
+
pads.push({
|
|
82
|
+
shape: "pill",
|
|
83
|
+
padId: smtpad.pcb_smtpad_id,
|
|
84
|
+
layer: smtpad.layer,
|
|
85
|
+
connectivityKey,
|
|
86
|
+
x: smtpad.x,
|
|
87
|
+
y: smtpad.y,
|
|
88
|
+
width: smtpad.width!,
|
|
89
|
+
height: smtpad.height!,
|
|
90
|
+
radius: smtpad.radius!,
|
|
91
|
+
ccwRotation:
|
|
92
|
+
smtpad.shape === "rotated_pill" ? smtpad.ccw_rotation : 0,
|
|
93
|
+
} as InputPillPad)
|
|
84
94
|
}
|
|
85
95
|
} else if (elm.type === "pcb_plated_hole") {
|
|
86
96
|
const platedHole = elm as PcbPlatedHole
|
|
87
97
|
if (!platedHole.layers.includes(options.layer)) continue
|
|
88
98
|
|
|
89
|
-
let connectivityKey =
|
|
99
|
+
let connectivityKey = getSubcircuitConnectivityKeyForId(
|
|
90
100
|
platedHole.pcb_plated_hole_id,
|
|
91
101
|
)
|
|
92
102
|
if (!connectivityKey) {
|
|
@@ -174,7 +184,7 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
174
184
|
if (!via.layers.includes(options.layer)) continue
|
|
175
185
|
|
|
176
186
|
const connectivityKey: string =
|
|
177
|
-
|
|
187
|
+
getSubcircuitConnectivityKeyForId(via.pcb_via_id) ??
|
|
178
188
|
`unconnected-via:${via.pcb_via_id}`
|
|
179
189
|
|
|
180
190
|
pads.push({
|
|
@@ -188,7 +198,7 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
188
198
|
} as InputCircularPad)
|
|
189
199
|
} else if (elm.type === "pcb_trace") {
|
|
190
200
|
const trace = elm as PcbTrace
|
|
191
|
-
const connectivityKey =
|
|
201
|
+
const connectivityKey = getSubcircuitConnectivityKeyForId(
|
|
192
202
|
trace.pcb_trace_id,
|
|
193
203
|
)
|
|
194
204
|
if (!connectivityKey) continue
|
|
@@ -256,7 +266,7 @@ export const convertCircuitJsonToInputProblem = (
|
|
|
256
266
|
layer: options.layer,
|
|
257
267
|
bounds,
|
|
258
268
|
outline,
|
|
259
|
-
connectivityKey:
|
|
269
|
+
connectivityKey: pourConnectivityKey,
|
|
260
270
|
padMargin: options.pad_margin,
|
|
261
271
|
traceMargin: options.trace_margin,
|
|
262
272
|
board_edge_margin: options.board_edge_margin ?? 0,
|