@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 CHANGED
@@ -1,37 +1,156 @@
1
1
  # @tscircuit/copper-pour-solver
2
2
 
3
- Solves for copper pour polygons
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
- ```tsx
6
- import { CopperPourPipelineSolver } from "@tscircuit/copper-pour-solver"
6
+ ## Install
7
7
 
8
- const solver = new CopperPourPipelineSolver({
9
- // Circuit JSON including pcb_plated_hole, pcb_hole, pcb_smtpad, pcb_trace, pcb_keepout etc.
10
- circuitJson
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.solve()
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
- solver.getOutput()
16
- // { brepShapes }
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
- ## B-Rep Shapes
60
+ You can also pass the source net's stable `subcircuit_connectivity_map_key`
61
+ directly:
20
62
 
21
- We use the following representation for 2D b-rep shapes
63
+ ```ts
64
+ const gnd = circuitJson.find(
65
+ (element) => element.type === "source_net" && element.name === "GND",
66
+ )
22
67
 
23
- ```tsx
24
- interface BRepShape {
25
- outerRing: Ring // The outer boundary
26
- innerRings: Ring // The inner cutouts
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
- interface Ring {
30
- cwVertices: PointWithBulge[]
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
- interface PointWithBulge {
34
- x: number
35
- y: number
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
+ ```
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json.schemastore.org/cosmos-config",
3
3
  "plugins": ["react-cosmos-plugin-vite"],
4
- "fixtureFileSuffix": "page"
4
+ "fixtureFileSuffix": "page",
5
+ "exportPath": "cosmos-export"
5
6
  }
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, AnyCircuitElement, LayerRef, Point as Point$1 } from 'circuit-json';
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
- declare const convertCircuitJsonToInputProblem: (circuitJson: AnyCircuitElement[], options: {
68
+ interface ConvertCircuitJsonToInputProblemOptions {
60
69
  layer: LayerRef;
61
- pour_connectivity_key: string;
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
- }) => InputProblem;
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 = connectivityMap.getNetConnectedToId(
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 = connectivityMap.getNetConnectedToId(
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 = connectivityMap.getNetConnectedToId(via.pcb_via_id) ?? `unconnected-via:${via.pcb_via_id}`;
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 = connectivityMap.getNetConnectedToId(
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: options.pour_connectivity_key,
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 = connectivityMap.getNetConnectedToId(
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 = connectivityMap.getNetConnectedToId(
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
- connectivityMap.getNetConnectedToId(via.pcb_via_id) ??
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 = connectivityMap.getNetConnectedToId(
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: options.pour_connectivity_key,
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,