@tscircuit/copper-pour-solver 0.0.34 → 0.0.35

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
@@ -67,12 +67,21 @@ const gnd = circuitJson.find(
67
67
 
68
68
  const inputProblem = convertCircuitJsonToInputProblem(circuitJson, {
69
69
  layer: "top",
70
+ subcircuit_id: gnd.subcircuit_id,
70
71
  subcircuit_connectivity_map_key: gnd.subcircuit_connectivity_map_key,
71
72
  pad_margin: 0.4,
72
73
  trace_margin: 0.2,
73
74
  })
74
75
  ```
75
76
 
77
+ Pass `subcircuit_id` when selecting a net inside a subcircuit. The converter
78
+ considers that subcircuit and its child subcircuits, but it does not treat
79
+ matching child `subcircuit_connectivity_map_key` values as connected unless the
80
+ Circuit JSON connectivity actually connects them. Internally, the generated
81
+ `globalConnectivityMap` is kept separate from the scoped
82
+ `subcircuitConnectivityMap`; scoped solver connectivity keys are prefixed with
83
+ their subcircuit id.
84
+
76
85
  Do not generate or pass ids from `circuit-json-to-connectivity-map`. The
77
86
  converter handles PCB connectivity internally and normalizes it to stable
78
87
  `subcircuit_connectivity_map_key` values.
package/dist/index.d.ts CHANGED
@@ -67,6 +67,7 @@ declare const initializeManifoldGeometry: () => Promise<void>;
67
67
 
68
68
  interface ConvertCircuitJsonToInputProblemOptions {
69
69
  layer: LayerRef;
70
+ subcircuit_id?: string;
70
71
  source_net_id?: string;
71
72
  source_net_name?: string;
72
73
  subcircuit_connectivity_map_key?: string;
package/dist/index.js CHANGED
@@ -548,50 +548,170 @@ var getElementSubcircuitConnectivityKey = (element) => {
548
548
  };
549
549
 
550
550
  // lib/circuit-json/buildSubcircuitConnectivityLookup.ts
551
- var buildSubcircuitConnectivityLookup = (circuitJson, connectivityMap) => {
551
+ var getElementSubcircuitId = (element) => {
552
+ const subcircuitId = element.subcircuit_id;
553
+ return typeof subcircuitId === "string" && subcircuitId.length > 0 ? subcircuitId : void 0;
554
+ };
555
+ var getScopedSubcircuitConnectivityKey = (subcircuitId, subcircuitConnectivityMapKey) => subcircuitId ? `subcircuit:${subcircuitId}:connectivity:${subcircuitConnectivityMapKey}` : subcircuitConnectivityMapKey;
556
+ var getDescendantSubcircuitIds = (circuitJson, rootSubcircuitId) => {
557
+ if (!rootSubcircuitId) return void 0;
558
+ const sourceGroupIdToSubcircuitId = {};
559
+ for (const element of circuitJson) {
560
+ if (element.type !== "source_group") continue;
561
+ const sourceGroupId = element.source_group_id;
562
+ const subcircuitId = getElementSubcircuitId(element);
563
+ if (typeof sourceGroupId === "string" && subcircuitId) {
564
+ sourceGroupIdToSubcircuitId[sourceGroupId] = subcircuitId;
565
+ }
566
+ }
567
+ const descendantSubcircuitIds = /* @__PURE__ */ new Set([rootSubcircuitId]);
568
+ let changed = true;
569
+ while (changed) {
570
+ changed = false;
571
+ for (const element of circuitJson) {
572
+ if (element.type !== "source_group") continue;
573
+ const subcircuitId = getElementSubcircuitId(element);
574
+ if (!subcircuitId || descendantSubcircuitIds.has(subcircuitId)) continue;
575
+ const parentSubcircuitId = element.parent_subcircuit_id;
576
+ const parentSourceGroupId = element.parent_source_group_id;
577
+ const parentSourceGroupSubcircuitId = typeof parentSourceGroupId === "string" ? sourceGroupIdToSubcircuitId[parentSourceGroupId] : void 0;
578
+ if (typeof parentSubcircuitId === "string" && descendantSubcircuitIds.has(parentSubcircuitId) || parentSourceGroupSubcircuitId && descendantSubcircuitIds.has(parentSourceGroupSubcircuitId)) {
579
+ descendantSubcircuitIds.add(subcircuitId);
580
+ changed = true;
581
+ }
582
+ }
583
+ }
584
+ return descendantSubcircuitIds;
585
+ };
586
+ var buildSubcircuitConnectivityLookup = (circuitJson, globalConnectivityMap, rootSubcircuitId) => {
587
+ const descendantSubcircuitIds = getDescendantSubcircuitIds(
588
+ circuitJson,
589
+ rootSubcircuitId
590
+ );
552
591
  const idToSubcircuitConnectivityKey = {};
592
+ const idToSubcircuitId = {};
593
+ const scopedKeyToIds = {};
594
+ const localSubcircuitConnectivityKeys = /* @__PURE__ */ new Set();
553
595
  for (const element of circuitJson) {
554
596
  const id = getElementId(element);
555
597
  const key = getElementSubcircuitConnectivityKey(element);
598
+ const subcircuitId = getElementSubcircuitId(element);
599
+ if (id) {
600
+ idToSubcircuitId[id] = subcircuitId;
601
+ }
602
+ if (descendantSubcircuitIds && (!subcircuitId || !descendantSubcircuitIds.has(subcircuitId))) {
603
+ continue;
604
+ }
556
605
  if (id && key) {
557
- idToSubcircuitConnectivityKey[id] = key;
606
+ const scopedKey = getScopedSubcircuitConnectivityKey(subcircuitId, key);
607
+ idToSubcircuitConnectivityKey[id] = scopedKey;
608
+ localSubcircuitConnectivityKeys.add(key);
609
+ scopedKeyToIds[scopedKey] ??= [];
610
+ scopedKeyToIds[scopedKey].push(id);
558
611
  }
559
612
  }
560
613
  const generatedNetIdToSubcircuitConnectivityKey = {};
561
614
  for (const [generatedNetId, connectedIds] of Object.entries(
562
- connectivityMap.netMap
615
+ globalConnectivityMap.netMap
563
616
  )) {
564
617
  const connectedSubcircuitKeys = new Set(
565
618
  connectedIds.map((id) => idToSubcircuitConnectivityKey[id]).filter((key) => Boolean(key))
566
619
  );
567
- if (connectedSubcircuitKeys.size > 1) {
620
+ const subcircuitKey = Array.from(connectedSubcircuitKeys).sort()[0];
621
+ if (subcircuitKey) {
622
+ generatedNetIdToSubcircuitConnectivityKey[generatedNetId] = subcircuitKey;
623
+ }
624
+ }
625
+ const subcircuitConnectivityMap = {};
626
+ for (const [scopedKey, ids] of Object.entries(scopedKeyToIds)) {
627
+ const resolvedKeys = /* @__PURE__ */ new Set();
628
+ for (const id of ids) {
629
+ const generatedNetId = globalConnectivityMap.getNetConnectedToId(id);
630
+ const resolvedKey2 = generatedNetId ? generatedNetIdToSubcircuitConnectivityKey[generatedNetId] : void 0;
631
+ resolvedKeys.add(resolvedKey2 ?? scopedKey);
632
+ }
633
+ if (resolvedKeys.size > 1) {
568
634
  throw new Error(
569
- `Multiple subcircuit connectivity keys found for generated connectivity map net "${generatedNetId}": ${Array.from(connectedSubcircuitKeys).join(", ")}`
635
+ `subcircuit_connectivity_map_key "${scopedKey}" maps to multiple global connectivity keys: ${Array.from(resolvedKeys).join(", ")}`
570
636
  );
571
637
  }
572
- const subcircuitKey = connectedSubcircuitKeys.values().next().value;
573
- if (subcircuitKey) {
574
- generatedNetIdToSubcircuitConnectivityKey[generatedNetId] = subcircuitKey;
638
+ const resolvedKey = resolvedKeys.values().next().value;
639
+ if (resolvedKey) {
640
+ subcircuitConnectivityMap[scopedKey] = resolvedKey;
575
641
  }
576
642
  }
577
643
  return {
578
- knownSubcircuitConnectivityKeys: new Set(
579
- Object.values(idToSubcircuitConnectivityKey)
580
- ),
644
+ knownSubcircuitConnectivityKeys: /* @__PURE__ */ new Set([
645
+ ...Object.keys(scopedKeyToIds),
646
+ ...localSubcircuitConnectivityKeys
647
+ ]),
648
+ descendantSubcircuitIds,
649
+ getScopedSubcircuitConnectivityKey,
650
+ getElementSubcircuitId,
651
+ resolveSubcircuitConnectivityKey(subcircuitConnectivityMapKey, subcircuitId) {
652
+ const matchingScopedKeys = Object.keys(scopedKeyToIds).filter(
653
+ (scopedKey) => scopedKey === getScopedSubcircuitConnectivityKey(
654
+ subcircuitId,
655
+ subcircuitConnectivityMapKey
656
+ )
657
+ );
658
+ if (subcircuitId && matchingScopedKeys.length === 0 && descendantSubcircuitIds) {
659
+ matchingScopedKeys.push(
660
+ ...Object.keys(scopedKeyToIds).filter(
661
+ (scopedKey) => scopedKey.endsWith(`:connectivity:${subcircuitConnectivityMapKey}`)
662
+ )
663
+ );
664
+ }
665
+ if (!subcircuitId) {
666
+ matchingScopedKeys.push(
667
+ ...Object.keys(scopedKeyToIds).filter(
668
+ (scopedKey) => scopedKey.endsWith(`:connectivity:${subcircuitConnectivityMapKey}`)
669
+ )
670
+ );
671
+ }
672
+ const uniqueMatchingScopedKeys = Array.from(new Set(matchingScopedKeys));
673
+ if (uniqueMatchingScopedKeys.length === 0) {
674
+ if (subcircuitId && descendantSubcircuitIds) {
675
+ throw new Error(
676
+ `No subcircuit_connectivity_map_key "${subcircuitConnectivityMapKey}" found in subcircuit "${subcircuitId}" or its child subcircuits.`
677
+ );
678
+ }
679
+ return getScopedSubcircuitConnectivityKey(
680
+ subcircuitId,
681
+ subcircuitConnectivityMapKey
682
+ );
683
+ }
684
+ if (uniqueMatchingScopedKeys.length > 1) {
685
+ throw new Error(
686
+ `subcircuit_connectivity_map_key "${subcircuitConnectivityMapKey}" exists in multiple subcircuits. Pass subcircuit_id to disambiguate.`
687
+ );
688
+ }
689
+ return subcircuitConnectivityMap[uniqueMatchingScopedKeys[0]] ?? uniqueMatchingScopedKeys[0];
690
+ },
581
691
  getSubcircuitConnectivityKeyForId(id) {
692
+ if (descendantSubcircuitIds) {
693
+ const subcircuitId = idToSubcircuitId[id];
694
+ if (!subcircuitId || !descendantSubcircuitIds.has(subcircuitId)) {
695
+ return void 0;
696
+ }
697
+ }
582
698
  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];
699
+ const generatedNetId = globalConnectivityMap.getNetConnectedToId(id);
700
+ if (!generatedNetId) {
701
+ return directKey ? subcircuitConnectivityMap[directKey] ?? directKey : void 0;
702
+ }
703
+ return generatedNetIdToSubcircuitConnectivityKey[generatedNetId] ?? (directKey ? subcircuitConnectivityMap[directKey] : void 0) ?? directKey;
587
704
  }
588
705
  };
589
706
  };
590
707
 
591
708
  // lib/circuit-json/resolvePourConnectivityKey.ts
592
- var resolvePourConnectivityKey = (circuitJson, options, knownSubcircuitConnectivityKeys) => {
709
+ var resolvePourConnectivityKey = (circuitJson, options, subcircuitConnectivityMap) => {
593
710
  if (options.subcircuit_connectivity_map_key) {
594
- return options.subcircuit_connectivity_map_key;
711
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
712
+ options.subcircuit_connectivity_map_key,
713
+ options.subcircuit_id
714
+ );
595
715
  }
596
716
  if (options.source_net_id) {
597
717
  const sourceNet = circuitJson.find(
@@ -605,11 +725,18 @@ var resolvePourConnectivityKey = (circuitJson, options, knownSubcircuitConnectiv
605
725
  `source_net "${options.source_net_id}" has no subcircuit_connectivity_map_key`
606
726
  );
607
727
  }
608
- return sourceNet.subcircuit_connectivity_map_key;
728
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
729
+ sourceNet.subcircuit_connectivity_map_key,
730
+ sourceNet.subcircuit_id ?? options.subcircuit_id
731
+ );
609
732
  }
610
733
  if (options.source_net_name) {
611
734
  const sourceNet = circuitJson.find(
612
- (element) => element.type === "source_net" && element.name === options.source_net_name
735
+ (element) => element.type === "source_net" && element.name === options.source_net_name && (!options.subcircuit_id || Boolean(
736
+ subcircuitConnectivityMap.descendantSubcircuitIds?.has(
737
+ element.subcircuit_id ?? ""
738
+ )
739
+ ))
613
740
  );
614
741
  if (!sourceNet) {
615
742
  throw new Error(
@@ -621,15 +748,23 @@ var resolvePourConnectivityKey = (circuitJson, options, knownSubcircuitConnectiv
621
748
  `source_net "${options.source_net_name}" has no subcircuit_connectivity_map_key`
622
749
  );
623
750
  }
624
- return sourceNet.subcircuit_connectivity_map_key;
751
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
752
+ sourceNet.subcircuit_connectivity_map_key,
753
+ sourceNet.subcircuit_id ?? options.subcircuit_id
754
+ );
625
755
  }
626
756
  if (options.pour_connectivity_key) {
627
- if (!knownSubcircuitConnectivityKeys.has(options.pour_connectivity_key)) {
757
+ if (!subcircuitConnectivityMap.knownSubcircuitConnectivityKeys.has(
758
+ options.pour_connectivity_key
759
+ )) {
628
760
  throw new Error(
629
761
  `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
762
  );
631
763
  }
632
- return options.pour_connectivity_key;
764
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
765
+ options.pour_connectivity_key,
766
+ options.subcircuit_id
767
+ );
633
768
  }
634
769
  throw new Error(
635
770
  "Copper pour requires source_net_id, source_net_name, or subcircuit_connectivity_map_key"
@@ -640,13 +775,18 @@ var resolvePourConnectivityKey = (circuitJson, options, knownSubcircuitConnectiv
640
775
  var convertCircuitJsonToInputProblem = (circuitJson, options) => {
641
776
  const pcb_board = circuitJson.find((e) => e.type === "pcb_board");
642
777
  if (!pcb_board) throw new Error("No pcb_board found in circuit json");
643
- const connectivityMap = getFullConnectivityMapFromCircuitJson(circuitJson);
644
- const { knownSubcircuitConnectivityKeys, getSubcircuitConnectivityKeyForId } = buildSubcircuitConnectivityLookup(circuitJson, connectivityMap);
778
+ const globalConnectivityMap = getFullConnectivityMapFromCircuitJson(circuitJson);
779
+ const subcircuitConnectivityMap = buildSubcircuitConnectivityLookup(
780
+ circuitJson,
781
+ globalConnectivityMap,
782
+ options.subcircuit_id
783
+ );
645
784
  const pourConnectivityKey = resolvePourConnectivityKey(
646
785
  circuitJson,
647
786
  options,
648
- knownSubcircuitConnectivityKeys
787
+ subcircuitConnectivityMap
649
788
  );
789
+ const { getSubcircuitConnectivityKeyForId } = subcircuitConnectivityMap;
650
790
  const pads = [];
651
791
  for (const elm of circuitJson) {
652
792
  if (elm.type === "pcb_smtpad") {
@@ -2,6 +2,7 @@ import type { LayerRef, Point } from "circuit-json"
2
2
 
3
3
  export interface ConvertCircuitJsonToInputProblemOptions {
4
4
  layer: LayerRef
5
+ subcircuit_id?: string
5
6
  source_net_id?: string
6
7
  source_net_name?: string
7
8
  subcircuit_connectivity_map_key?: string
@@ -3,52 +3,235 @@ import type { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-conn
3
3
  import { getElementId } from "@tscircuit/circuit-json-util"
4
4
  import { getElementSubcircuitConnectivityKey } from "./getElementSubcircuitConnectivityKey"
5
5
 
6
+ const getElementSubcircuitId = (
7
+ element: AnyCircuitElement,
8
+ ): string | undefined => {
9
+ const subcircuitId = (element as any).subcircuit_id
10
+ return typeof subcircuitId === "string" && subcircuitId.length > 0
11
+ ? subcircuitId
12
+ : undefined
13
+ }
14
+
15
+ const getScopedSubcircuitConnectivityKey = (
16
+ subcircuitId: string | undefined,
17
+ subcircuitConnectivityMapKey: string,
18
+ ): string =>
19
+ subcircuitId
20
+ ? `subcircuit:${subcircuitId}:connectivity:${subcircuitConnectivityMapKey}`
21
+ : subcircuitConnectivityMapKey
22
+
23
+ const getDescendantSubcircuitIds = (
24
+ circuitJson: AnyCircuitElement[],
25
+ rootSubcircuitId: string | undefined,
26
+ ): Set<string> | undefined => {
27
+ if (!rootSubcircuitId) return undefined
28
+
29
+ const sourceGroupIdToSubcircuitId: Record<string, string> = {}
30
+ for (const element of circuitJson) {
31
+ if (element.type !== "source_group") continue
32
+
33
+ const sourceGroupId = (element as any).source_group_id
34
+ const subcircuitId = getElementSubcircuitId(element)
35
+ if (typeof sourceGroupId === "string" && subcircuitId) {
36
+ sourceGroupIdToSubcircuitId[sourceGroupId] = subcircuitId
37
+ }
38
+ }
39
+
40
+ const descendantSubcircuitIds = new Set([rootSubcircuitId])
41
+ let changed = true
42
+ while (changed) {
43
+ changed = false
44
+ for (const element of circuitJson) {
45
+ if (element.type !== "source_group") continue
46
+
47
+ const subcircuitId = getElementSubcircuitId(element)
48
+ if (!subcircuitId || descendantSubcircuitIds.has(subcircuitId)) continue
49
+
50
+ const parentSubcircuitId = (element as any).parent_subcircuit_id
51
+ const parentSourceGroupId = (element as any).parent_source_group_id
52
+ const parentSourceGroupSubcircuitId =
53
+ typeof parentSourceGroupId === "string"
54
+ ? sourceGroupIdToSubcircuitId[parentSourceGroupId]
55
+ : undefined
56
+
57
+ if (
58
+ (typeof parentSubcircuitId === "string" &&
59
+ descendantSubcircuitIds.has(parentSubcircuitId)) ||
60
+ (parentSourceGroupSubcircuitId &&
61
+ descendantSubcircuitIds.has(parentSourceGroupSubcircuitId))
62
+ ) {
63
+ descendantSubcircuitIds.add(subcircuitId)
64
+ changed = true
65
+ }
66
+ }
67
+ }
68
+
69
+ return descendantSubcircuitIds
70
+ }
71
+
6
72
  export const buildSubcircuitConnectivityLookup = (
7
73
  circuitJson: AnyCircuitElement[],
8
- connectivityMap: ReturnType<typeof getFullConnectivityMapFromCircuitJson>,
74
+ globalConnectivityMap: ReturnType<
75
+ typeof getFullConnectivityMapFromCircuitJson
76
+ >,
77
+ rootSubcircuitId?: string,
9
78
  ) => {
79
+ const descendantSubcircuitIds = getDescendantSubcircuitIds(
80
+ circuitJson,
81
+ rootSubcircuitId,
82
+ )
10
83
  const idToSubcircuitConnectivityKey: Record<string, string> = {}
84
+ const idToSubcircuitId: Record<string, string | undefined> = {}
85
+ const scopedKeyToIds: Record<string, string[]> = {}
86
+ const localSubcircuitConnectivityKeys = new Set<string>()
11
87
 
12
88
  for (const element of circuitJson) {
13
89
  const id = getElementId(element)
14
90
  const key = getElementSubcircuitConnectivityKey(element)
91
+ const subcircuitId = getElementSubcircuitId(element)
92
+ if (id) {
93
+ idToSubcircuitId[id] = subcircuitId
94
+ }
95
+ if (
96
+ descendantSubcircuitIds &&
97
+ (!subcircuitId || !descendantSubcircuitIds.has(subcircuitId))
98
+ ) {
99
+ continue
100
+ }
101
+
15
102
  if (id && key) {
16
- idToSubcircuitConnectivityKey[id] = key
103
+ const scopedKey = getScopedSubcircuitConnectivityKey(subcircuitId, key)
104
+ idToSubcircuitConnectivityKey[id] = scopedKey
105
+ localSubcircuitConnectivityKeys.add(key)
106
+ scopedKeyToIds[scopedKey] ??= []
107
+ scopedKeyToIds[scopedKey].push(id)
17
108
  }
18
109
  }
19
110
 
20
111
  const generatedNetIdToSubcircuitConnectivityKey: Record<string, string> = {}
21
112
  for (const [generatedNetId, connectedIds] of Object.entries(
22
- connectivityMap.netMap,
113
+ globalConnectivityMap.netMap,
23
114
  )) {
24
115
  const connectedSubcircuitKeys = new Set(
25
116
  connectedIds
26
117
  .map((id) => idToSubcircuitConnectivityKey[id])
27
118
  .filter((key): key is string => Boolean(key)),
28
119
  )
29
- if (connectedSubcircuitKeys.size > 1) {
120
+ const subcircuitKey = Array.from(connectedSubcircuitKeys).sort()[0]
121
+ if (subcircuitKey) {
122
+ generatedNetIdToSubcircuitConnectivityKey[generatedNetId] = subcircuitKey
123
+ }
124
+ }
125
+
126
+ const subcircuitConnectivityMap: Record<string, string> = {}
127
+ for (const [scopedKey, ids] of Object.entries(scopedKeyToIds)) {
128
+ const resolvedKeys = new Set<string>()
129
+ for (const id of ids) {
130
+ const generatedNetId = globalConnectivityMap.getNetConnectedToId(id)
131
+ const resolvedKey = generatedNetId
132
+ ? generatedNetIdToSubcircuitConnectivityKey[generatedNetId]
133
+ : undefined
134
+ resolvedKeys.add(resolvedKey ?? scopedKey)
135
+ }
136
+
137
+ if (resolvedKeys.size > 1) {
30
138
  throw new Error(
31
- `Multiple subcircuit connectivity keys found for generated connectivity map net "${generatedNetId}": ${Array.from(connectedSubcircuitKeys).join(", ")}`,
139
+ `subcircuit_connectivity_map_key "${scopedKey}" maps to multiple global connectivity keys: ${Array.from(resolvedKeys).join(", ")}`,
32
140
  )
33
141
  }
34
- const subcircuitKey = connectedSubcircuitKeys.values().next().value
35
- if (subcircuitKey) {
36
- generatedNetIdToSubcircuitConnectivityKey[generatedNetId] = subcircuitKey
142
+
143
+ const resolvedKey = resolvedKeys.values().next().value
144
+ if (resolvedKey) {
145
+ subcircuitConnectivityMap[scopedKey] = resolvedKey
37
146
  }
38
147
  }
39
148
 
40
149
  return {
41
- knownSubcircuitConnectivityKeys: new Set(
42
- Object.values(idToSubcircuitConnectivityKey),
43
- ),
150
+ knownSubcircuitConnectivityKeys: new Set([
151
+ ...Object.keys(scopedKeyToIds),
152
+ ...localSubcircuitConnectivityKeys,
153
+ ]),
154
+ descendantSubcircuitIds,
155
+ getScopedSubcircuitConnectivityKey,
156
+ getElementSubcircuitId,
157
+ resolveSubcircuitConnectivityKey(
158
+ subcircuitConnectivityMapKey: string,
159
+ subcircuitId?: string,
160
+ ): string {
161
+ const matchingScopedKeys = Object.keys(scopedKeyToIds).filter(
162
+ (scopedKey) =>
163
+ scopedKey ===
164
+ getScopedSubcircuitConnectivityKey(
165
+ subcircuitId,
166
+ subcircuitConnectivityMapKey,
167
+ ),
168
+ )
169
+
170
+ if (
171
+ subcircuitId &&
172
+ matchingScopedKeys.length === 0 &&
173
+ descendantSubcircuitIds
174
+ ) {
175
+ matchingScopedKeys.push(
176
+ ...Object.keys(scopedKeyToIds).filter((scopedKey) =>
177
+ scopedKey.endsWith(`:connectivity:${subcircuitConnectivityMapKey}`),
178
+ ),
179
+ )
180
+ }
181
+
182
+ if (!subcircuitId) {
183
+ matchingScopedKeys.push(
184
+ ...Object.keys(scopedKeyToIds).filter((scopedKey) =>
185
+ scopedKey.endsWith(`:connectivity:${subcircuitConnectivityMapKey}`),
186
+ ),
187
+ )
188
+ }
189
+
190
+ const uniqueMatchingScopedKeys = Array.from(new Set(matchingScopedKeys))
191
+ if (uniqueMatchingScopedKeys.length === 0) {
192
+ if (subcircuitId && descendantSubcircuitIds) {
193
+ throw new Error(
194
+ `No subcircuit_connectivity_map_key "${subcircuitConnectivityMapKey}" found in subcircuit "${subcircuitId}" or its child subcircuits.`,
195
+ )
196
+ }
197
+
198
+ return getScopedSubcircuitConnectivityKey(
199
+ subcircuitId,
200
+ subcircuitConnectivityMapKey,
201
+ )
202
+ }
203
+ if (uniqueMatchingScopedKeys.length > 1) {
204
+ throw new Error(
205
+ `subcircuit_connectivity_map_key "${subcircuitConnectivityMapKey}" exists in multiple subcircuits. Pass subcircuit_id to disambiguate.`,
206
+ )
207
+ }
208
+
209
+ return (
210
+ subcircuitConnectivityMap[uniqueMatchingScopedKeys[0]!] ??
211
+ uniqueMatchingScopedKeys[0]!
212
+ )
213
+ },
44
214
  getSubcircuitConnectivityKeyForId(id: string): string | undefined {
45
- const directKey = idToSubcircuitConnectivityKey[id]
46
- if (directKey) return directKey
215
+ if (descendantSubcircuitIds) {
216
+ const subcircuitId = idToSubcircuitId[id]
217
+ if (!subcircuitId || !descendantSubcircuitIds.has(subcircuitId)) {
218
+ return undefined
219
+ }
220
+ }
47
221
 
48
- const generatedNetId = connectivityMap.getNetConnectedToId(id)
49
- if (!generatedNetId) return undefined
222
+ const directKey = idToSubcircuitConnectivityKey[id]
223
+ const generatedNetId = globalConnectivityMap.getNetConnectedToId(id)
224
+ if (!generatedNetId) {
225
+ return directKey
226
+ ? (subcircuitConnectivityMap[directKey] ?? directKey)
227
+ : undefined
228
+ }
50
229
 
51
- return generatedNetIdToSubcircuitConnectivityKey[generatedNetId]
230
+ return (
231
+ generatedNetIdToSubcircuitConnectivityKey[generatedNetId] ??
232
+ (directKey ? subcircuitConnectivityMap[directKey] : undefined) ??
233
+ directKey
234
+ )
52
235
  },
53
236
  }
54
237
  }
@@ -32,14 +32,19 @@ export const convertCircuitJsonToInputProblem = (
32
32
 
33
33
  if (!pcb_board) throw new Error("No pcb_board found in circuit json")
34
34
 
35
- const connectivityMap = getFullConnectivityMapFromCircuitJson(circuitJson)
36
- const { knownSubcircuitConnectivityKeys, getSubcircuitConnectivityKeyForId } =
37
- buildSubcircuitConnectivityLookup(circuitJson, connectivityMap)
35
+ const globalConnectivityMap =
36
+ getFullConnectivityMapFromCircuitJson(circuitJson)
37
+ const subcircuitConnectivityMap = buildSubcircuitConnectivityLookup(
38
+ circuitJson,
39
+ globalConnectivityMap,
40
+ options.subcircuit_id,
41
+ )
38
42
  const pourConnectivityKey = resolvePourConnectivityKey(
39
43
  circuitJson,
40
44
  options,
41
- knownSubcircuitConnectivityKeys,
45
+ subcircuitConnectivityMap,
42
46
  )
47
+ const { getSubcircuitConnectivityKeyForId } = subcircuitConnectivityMap
43
48
 
44
49
  const pads: InputPad[] = []
45
50
 
@@ -1,13 +1,21 @@
1
1
  import type { AnyCircuitElement, SourceNet } from "circuit-json"
2
+ import type { buildSubcircuitConnectivityLookup } from "./buildSubcircuitConnectivityLookup"
2
3
  import type { ConvertCircuitJsonToInputProblemOptions } from "./ConvertCircuitJsonToInputProblemOptions"
3
4
 
5
+ type SubcircuitConnectivityLookup = ReturnType<
6
+ typeof buildSubcircuitConnectivityLookup
7
+ >
8
+
4
9
  export const resolvePourConnectivityKey = (
5
10
  circuitJson: AnyCircuitElement[],
6
11
  options: ConvertCircuitJsonToInputProblemOptions,
7
- knownSubcircuitConnectivityKeys: Set<string>,
12
+ subcircuitConnectivityMap: SubcircuitConnectivityLookup,
8
13
  ): string => {
9
14
  if (options.subcircuit_connectivity_map_key) {
10
- return options.subcircuit_connectivity_map_key
15
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
16
+ options.subcircuit_connectivity_map_key,
17
+ options.subcircuit_id,
18
+ )
11
19
  }
12
20
 
13
21
  if (options.source_net_id) {
@@ -24,14 +32,23 @@ export const resolvePourConnectivityKey = (
24
32
  `source_net "${options.source_net_id}" has no subcircuit_connectivity_map_key`,
25
33
  )
26
34
  }
27
- return sourceNet.subcircuit_connectivity_map_key
35
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
36
+ sourceNet.subcircuit_connectivity_map_key,
37
+ sourceNet.subcircuit_id ?? options.subcircuit_id,
38
+ )
28
39
  }
29
40
 
30
41
  if (options.source_net_name) {
31
42
  const sourceNet = circuitJson.find(
32
43
  (element): element is SourceNet =>
33
44
  element.type === "source_net" &&
34
- element.name === options.source_net_name,
45
+ element.name === options.source_net_name &&
46
+ (!options.subcircuit_id ||
47
+ Boolean(
48
+ subcircuitConnectivityMap.descendantSubcircuitIds?.has(
49
+ element.subcircuit_id ?? "",
50
+ ),
51
+ )),
35
52
  )
36
53
  if (!sourceNet) {
37
54
  throw new Error(
@@ -43,16 +60,26 @@ export const resolvePourConnectivityKey = (
43
60
  `source_net "${options.source_net_name}" has no subcircuit_connectivity_map_key`,
44
61
  )
45
62
  }
46
- return sourceNet.subcircuit_connectivity_map_key
63
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
64
+ sourceNet.subcircuit_connectivity_map_key,
65
+ sourceNet.subcircuit_id ?? options.subcircuit_id,
66
+ )
47
67
  }
48
68
 
49
69
  if (options.pour_connectivity_key) {
50
- if (!knownSubcircuitConnectivityKeys.has(options.pour_connectivity_key)) {
70
+ if (
71
+ !subcircuitConnectivityMap.knownSubcircuitConnectivityKeys.has(
72
+ options.pour_connectivity_key,
73
+ )
74
+ ) {
51
75
  throw new Error(
52
76
  `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.`,
53
77
  )
54
78
  }
55
- return options.pour_connectivity_key
79
+ return subcircuitConnectivityMap.resolveSubcircuitConnectivityKey(
80
+ options.pour_connectivity_key,
81
+ options.subcircuit_id,
82
+ )
56
83
  }
57
84
 
58
85
  throw new Error(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/copper-pour-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.34",
4
+ "version": "0.0.35",
5
5
  "scripts": {
6
6
  "format": "biome format . --write",
7
7
  "format:check": "biome format .",
@@ -13,14 +13,15 @@
13
13
  "type": "module",
14
14
  "devDependencies": {
15
15
  "@biomejs/biome": "^2.2.4",
16
- "@tscircuit/circuit-json-util": "^0.0.77",
17
16
  "@flatten-js/core": "^1.6.2",
18
- "@tscircuit/math-utils": "^0.0.25",
17
+ "@tscircuit/circuit-json-util": "^0.0.77",
18
+ "@tscircuit/core": "^0.0.988",
19
+ "@tscircuit/math-utils": "^0.0.29",
19
20
  "@tscircuit/solver-utils": "^0.0.14",
20
- "circuit-json-to-connectivity-map": "^0.0.23",
21
21
  "@types/bun": "latest",
22
22
  "bun-match-svg": "^0.0.13",
23
23
  "circuit-json": "^0.0.432",
24
+ "circuit-json-to-connectivity-map": "^0.0.23",
24
25
  "circuit-to-svg": "^0.0.350",
25
26
  "react-cosmos": "^7.0.0",
26
27
  "react-cosmos-plugin-vite": "^7.0.0",
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="600" data-software-used-string="@tscircuit/core@0.0.988"><style></style><rect class="boundary" x="0" y="0" fill="#000" width="800" height="600" data-type="pcb_background" data-pcb-layer="global"/><rect class="pcb-boundary" fill="none" stroke="#fff" stroke-width="0.3" x="66.66666666666669" y="66.6666666666666" width="666.6666666666667" height="466.66666666666663" data-type="pcb_boundary" data-pcb-layer="global"/><path class="pcb-board" d="M 66.66666666666669 533.3333333333333 L 733.3333333333335 533.3333333333333 L 733.3333333333335 66.6666666666666 L 66.66666666666669 66.6666666666666 Z" fill="none" stroke="rgba(255, 255, 255, 0.5)" stroke-width="6.666666666666668" data-type="pcb_board" data-pcb-layer="board"/><path class="pcb-trace" stroke="rgb(200, 52, 52)" fill="none" d="M 200.00000000000006 299.99999999999994 L 332.00000000000006 299.99999999999994" stroke-width="10.666666666666668" stroke-linecap="round" stroke-linejoin="round" shape-rendering="crispEdges" data-type="pcb_trace" data-pcb-layer="top"/><path class="pcb-trace" stroke="rgb(200, 52, 52)" fill="none" d="M 332.00000000000006 299.99999999999994 L 332.00000000000006 183.33333333333326" stroke-width="10.666666666666668" stroke-linecap="round" stroke-linejoin="round" shape-rendering="crispEdges" data-type="pcb_trace" data-pcb-layer="top"/><path class="pcb-trace" stroke="rgb(200, 52, 52)" fill="none" d="M 332.00000000000006 183.33333333333326 L 618.0000000000001 183.33333333333326" stroke-width="10.666666666666668" stroke-linecap="round" stroke-linejoin="round" shape-rendering="crispEdges" data-type="pcb_trace" data-pcb-layer="top"/><path class="pcb-trace" stroke="rgb(200, 52, 52)" fill="none" d="M 618.0000000000001 183.33333333333326 L 618.0000000000001 299.99999999999994" stroke-width="10.666666666666668" stroke-linecap="round" stroke-linejoin="round" shape-rendering="crispEdges" data-type="pcb_trace" data-pcb-layer="top"/><path class="pcb-copper-pour pcb-copper-pour-brep" d="M 733.3333333333335 533.3333333333333 L 66.66666666666669 533.3333333333333 L 66.66666666666669 66.6666666666666 L 733.3333333333335 66.6666666666666 L 733.3333333333335 533.3333333333333 Z M 382.00000000000006 366.66666666666663 L 515.3333333333334 366.66666666666663 L 515.3333333333334 233.33333333333326 L 382.00000000000006 233.33333333333326 L 382.00000000000006 366.66666666666663 Z" fill="rgb(200, 52, 52)" fill-rule="evenodd" fill-opacity="0.5" data-type="pcb_copper_pour" data-pcb-layer="top"/><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="33.333333333333336" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,200.00000000000006,224.99999999999994)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_0" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">A</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="46.666666666666664" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,200.00000000000006,130.6666666666666)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_1" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">J1</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="33.333333333333336" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,448.66666666666674,224.99999999999994)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_2" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">B</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="33.333333333333336" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,618.0000000000001,224.99999999999994)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_3" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">C</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="46.666666666666664" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,533.3333333333334,130.6666666666666)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_4" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">J2</text><g data-type="pcb_plated_hole" data-pcb-layer="through"><rect class="pcb-hole-outer-pad" fill="rgb(200, 52, 52)" x="150.00000000000006" y="249.99999999999994" width="100" height="100" data-type="pcb_plated_hole" data-pcb-layer="top"/><circle class="pcb-hole-inner" fill="#FF26E2" cx="200.00000000000006" cy="299.99999999999994" r="33.333333333333336" data-type="pcb_plated_hole_drill" data-pcb-layer="drill"/></g><g data-type="pcb_plated_hole" data-pcb-layer="through"><rect class="pcb-hole-outer-pad" fill="rgb(200, 52, 52)" x="398.66666666666674" y="249.99999999999994" width="100" height="100" data-type="pcb_plated_hole" data-pcb-layer="top"/><circle class="pcb-hole-inner" fill="#FF26E2" cx="448.66666666666674" cy="299.99999999999994" r="33.333333333333336" data-type="pcb_plated_hole_drill" data-pcb-layer="drill"/></g><g data-type="pcb_plated_hole" data-pcb-layer="through"><circle class="pcb-hole-outer" fill="rgb(200, 52, 52)" cx="618.0000000000001" cy="299.99999999999994" r="50" data-type="pcb_plated_hole" data-pcb-layer="top"/><circle class="pcb-hole-inner" fill="#FF26E2" cx="618.0000000000001" cy="299.99999999999994" r="33.333333333333336" data-type="pcb_plated_hole_drill" data-pcb-layer="drill"/></g><text x="400.00000000000006" y="9.999999999999943" fill="#ffffff" font-family="Arial, sans-serif" font-size="14.666666666666668" text-anchor="middle" dominant-baseline="central" class="pcb-note-text" data-type="pcb_note_text" data-pcb-note-text-id="pcb_note_text_0" data-pcb-layer="overlay">Expected: copper pour connects to A and C, but clears around B.</text><text x="400.00000000000006" y="29.999999999999943" fill="#ffffff" font-family="Arial, sans-serif" font-size="14.666666666666668" text-anchor="middle" dominant-baseline="central" class="pcb-note-text" data-type="pcb_note_text" data-pcb-note-text-id="pcb_note_text_1" data-pcb-layer="overlay">A is parent net.GND; C is child net.GND reached through the parent-to-child net.</text><text x="400.00000000000006" y="49.999999999999915" fill="#ffffff" font-family="Arial, sans-serif" font-size="14.666666666666668" text-anchor="middle" dominant-baseline="central" class="pcb-note-text" data-type="pcb_note_text" data-pcb-note-text-id="pcb_note_text_2" data-pcb-layer="overlay">B is a separate child pin, so the pour should clear around it.</text></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="600" data-software-used-string="@tscircuit/core@0.0.988"><style></style><rect class="boundary" x="0" y="0" fill="#000" width="800" height="600" data-type="pcb_background" data-pcb-layer="global"/><rect class="pcb-boundary" fill="none" stroke="#fff" stroke-width="0.3" x="66.66666666666669" y="66.6666666666666" width="666.6666666666667" height="466.66666666666663" data-type="pcb_boundary" data-pcb-layer="global"/><path class="pcb-board" d="M 66.66666666666669 533.3333333333333 L 733.3333333333335 533.3333333333333 L 733.3333333333335 66.6666666666666 L 66.66666666666669 66.6666666666666 Z" fill="none" stroke="rgba(255, 255, 255, 0.5)" stroke-width="6.666666666666668" data-type="pcb_board" data-pcb-layer="board"/><path class="pcb-copper-pour pcb-copper-pour-brep" d="M 733.3333333333335 533.3333333333333 L 66.66666666666669 533.3333333333333 L 66.66666666666669 66.6666666666666 L 733.3333333333335 66.6666666666666 L 733.3333333333335 533.3333333333333 Z M 382.00000000000006 366.66666666666663 L 515.3333333333334 366.66666666666663 L 515.3333333333334 233.33333333333326 L 382.00000000000006 233.33333333333326 L 382.00000000000006 366.66666666666663 Z M 618.0000000000001 366.66666666666663 L 631.0060000000001 365.3856666666666 L 643.5122000000001 361.5919999999999 L 655.038 355.43133333333327 L 665.1404666666667 347.1404666666666 L 673.4313333333334 337.03799999999995 L 679.5920000000001 325.51219999999995 L 683.3856666666668 313.006 L 684.6666666666667 299.99999999999994 L 683.3856666666668 286.9939999999999 L 679.5920000000001 274.48779999999994 L 673.4313333333334 262.96199999999993 L 665.1404666666667 252.85953333333327 L 655.038 244.56866666666662 L 643.5122000000001 238.40799999999993 L 631.0060000000001 234.61433333333326 L 618.0000000000001 233.33333333333326 L 604.9940000000001 234.61433333333326 L 592.4878000000001 238.40799999999993 L 580.9620000000001 244.56866666666662 L 570.8595333333334 252.85953333333327 L 562.5686666666668 262.96199999999993 L 556.4080000000001 274.48779999999994 L 552.6143333333334 286.9939999999999 L 551.3333333333334 299.99999999999994 L 552.6143333333334 313.006 L 556.4080000000001 325.51219999999995 L 562.5686666666668 337.03799999999995 L 570.8595333333334 347.1404666666666 L 580.9620000000001 355.43133333333327 L 592.4878000000001 361.5919999999999 L 604.9940000000001 365.3856666666666 L 618.0000000000001 366.66666666666663 Z" fill="rgb(200, 52, 52)" fill-rule="evenodd" fill-opacity="0.5" data-type="pcb_copper_pour" data-pcb-layer="top"/><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="33.333333333333336" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,200.00000000000006,224.99999999999994)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_0" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">A</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="46.666666666666664" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,200.00000000000006,130.6666666666666)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_1" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">J1</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="33.333333333333336" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,448.66666666666674,224.99999999999994)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_2" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">B</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="33.333333333333336" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,618.0000000000001,224.99999999999994)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_3" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">C</text><text x="0" y="0" dx="0" dy="0" fill="#f2eda1" font-family="Arial, sans-serif" font-size="46.666666666666664" text-anchor="middle" dominant-baseline="central" transform="matrix(1,0,0,1,533.3333333333334,130.6666666666666)" class="pcb-silkscreen-text pcb-silkscreen-top" data-pcb-silkscreen-text-id="pcb_silkscreen_text_4" stroke="none" data-type="pcb_silkscreen_text" data-pcb-layer="top">J2</text><g data-type="pcb_plated_hole" data-pcb-layer="through"><rect class="pcb-hole-outer-pad" fill="rgb(200, 52, 52)" x="150.00000000000006" y="249.99999999999994" width="100" height="100" data-type="pcb_plated_hole" data-pcb-layer="top"/><circle class="pcb-hole-inner" fill="#FF26E2" cx="200.00000000000006" cy="299.99999999999994" r="33.333333333333336" data-type="pcb_plated_hole_drill" data-pcb-layer="drill"/></g><g data-type="pcb_plated_hole" data-pcb-layer="through"><rect class="pcb-hole-outer-pad" fill="rgb(200, 52, 52)" x="398.66666666666674" y="249.99999999999994" width="100" height="100" data-type="pcb_plated_hole" data-pcb-layer="top"/><circle class="pcb-hole-inner" fill="#FF26E2" cx="448.66666666666674" cy="299.99999999999994" r="33.333333333333336" data-type="pcb_plated_hole_drill" data-pcb-layer="drill"/></g><g data-type="pcb_plated_hole" data-pcb-layer="through"><circle class="pcb-hole-outer" fill="rgb(200, 52, 52)" cx="618.0000000000001" cy="299.99999999999994" r="50" data-type="pcb_plated_hole" data-pcb-layer="top"/><circle class="pcb-hole-inner" fill="#FF26E2" cx="618.0000000000001" cy="299.99999999999994" r="33.333333333333336" data-type="pcb_plated_hole_drill" data-pcb-layer="drill"/></g><text x="400.00000000000006" y="9.999999999999943" fill="#ffffff" font-family="Arial, sans-serif" font-size="14.666666666666668" text-anchor="middle" dominant-baseline="central" class="pcb-note-text" data-type="pcb_note_text" data-pcb-note-text-id="pcb_note_text_0" data-pcb-layer="overlay">Expected: copper pour connects to A, but clears around B and C.</text><text x="400.00000000000006" y="29.999999999999943" fill="#ffffff" font-family="Arial, sans-serif" font-size="14.666666666666668" text-anchor="middle" dominant-baseline="central" class="pcb-note-text" data-type="pcb_note_text" data-pcb-note-text-id="pcb_note_text_1" data-pcb-layer="overlay">A is parent net.GND; C is child net.GND with no parent-to-child net.</text><text x="400.00000000000006" y="49.999999999999915" fill="#ffffff" font-family="Arial, sans-serif" font-size="14.666666666666668" text-anchor="middle" dominant-baseline="central" class="pcb-note-text" data-type="pcb_note_text" data-pcb-note-text-id="pcb_note_text_2" data-pcb-layer="overlay">B is a separate child pin, so the pour should clear around it.</text></svg>
@@ -0,0 +1,126 @@
1
+ [
2
+ {
3
+ "type": "pcb_board",
4
+ "pcb_board_id": "pcb_board_0",
5
+ "center": { "x": 0, "y": 0 },
6
+ "width": 8,
7
+ "height": 4,
8
+ "thickness": 1.4,
9
+ "num_layers": 2,
10
+ "material": "fr4"
11
+ },
12
+ {
13
+ "type": "source_group",
14
+ "source_group_id": "source_group_parent",
15
+ "is_subcircuit": true,
16
+ "subcircuit_id": "subcircuit_parent"
17
+ },
18
+ {
19
+ "type": "source_group",
20
+ "source_group_id": "source_group_child_a",
21
+ "is_subcircuit": true,
22
+ "subcircuit_id": "subcircuit_child_a",
23
+ "parent_subcircuit_id": "subcircuit_parent"
24
+ },
25
+ {
26
+ "type": "source_group",
27
+ "source_group_id": "source_group_child_b",
28
+ "is_subcircuit": true,
29
+ "subcircuit_id": "subcircuit_child_b",
30
+ "parent_subcircuit_id": "subcircuit_parent"
31
+ },
32
+ {
33
+ "type": "source_net",
34
+ "source_net_id": "source_net_child_a_gnd",
35
+ "name": "GND",
36
+ "member_source_group_ids": ["source_group_child_a"],
37
+ "subcircuit_id": "subcircuit_child_a",
38
+ "subcircuit_connectivity_map_key": "net0"
39
+ },
40
+ {
41
+ "type": "source_trace",
42
+ "source_trace_id": "source_trace_child_a_gnd",
43
+ "connected_source_net_ids": ["source_net_child_a_gnd"],
44
+ "connected_source_port_ids": ["source_port_child_a_gnd"],
45
+ "subcircuit_id": "subcircuit_child_a",
46
+ "subcircuit_connectivity_map_key": "net0"
47
+ },
48
+ {
49
+ "type": "source_port",
50
+ "source_port_id": "source_port_child_a_gnd",
51
+ "source_component_id": "source_component_child_a",
52
+ "name": "1",
53
+ "subcircuit_id": "subcircuit_child_a",
54
+ "subcircuit_connectivity_map_key": "net0"
55
+ },
56
+ {
57
+ "type": "pcb_port",
58
+ "pcb_port_id": "pcb_port_child_a_gnd",
59
+ "pcb_component_id": "pcb_component_child_a",
60
+ "source_port_id": "source_port_child_a_gnd",
61
+ "x": -2,
62
+ "y": 0,
63
+ "layers": ["top"],
64
+ "subcircuit_id": "subcircuit_child_a"
65
+ },
66
+ {
67
+ "type": "pcb_smtpad",
68
+ "pcb_smtpad_id": "pcb_smtpad_child_a_gnd",
69
+ "pcb_component_id": "pcb_component_child_a",
70
+ "pcb_port_id": "pcb_port_child_a_gnd",
71
+ "layer": "top",
72
+ "shape": "rect",
73
+ "x": -2,
74
+ "y": 0,
75
+ "width": 0.8,
76
+ "height": 0.8,
77
+ "subcircuit_id": "subcircuit_child_a"
78
+ },
79
+ {
80
+ "type": "source_net",
81
+ "source_net_id": "source_net_child_b_gnd",
82
+ "name": "GND",
83
+ "member_source_group_ids": ["source_group_child_b"],
84
+ "subcircuit_id": "subcircuit_child_b",
85
+ "subcircuit_connectivity_map_key": "net0"
86
+ },
87
+ {
88
+ "type": "source_trace",
89
+ "source_trace_id": "source_trace_child_b_gnd",
90
+ "connected_source_net_ids": ["source_net_child_b_gnd"],
91
+ "connected_source_port_ids": ["source_port_child_b_gnd"],
92
+ "subcircuit_id": "subcircuit_child_b",
93
+ "subcircuit_connectivity_map_key": "net0"
94
+ },
95
+ {
96
+ "type": "source_port",
97
+ "source_port_id": "source_port_child_b_gnd",
98
+ "source_component_id": "source_component_child_b",
99
+ "name": "1",
100
+ "subcircuit_id": "subcircuit_child_b",
101
+ "subcircuit_connectivity_map_key": "net0"
102
+ },
103
+ {
104
+ "type": "pcb_port",
105
+ "pcb_port_id": "pcb_port_child_b_gnd",
106
+ "pcb_component_id": "pcb_component_child_b",
107
+ "source_port_id": "source_port_child_b_gnd",
108
+ "x": 2,
109
+ "y": 0,
110
+ "layers": ["top"],
111
+ "subcircuit_id": "subcircuit_child_b"
112
+ },
113
+ {
114
+ "type": "pcb_smtpad",
115
+ "pcb_smtpad_id": "pcb_smtpad_child_b_gnd",
116
+ "pcb_component_id": "pcb_component_child_b",
117
+ "pcb_port_id": "pcb_port_child_b_gnd",
118
+ "layer": "top",
119
+ "shape": "rect",
120
+ "x": 2,
121
+ "y": 0,
122
+ "width": 0.8,
123
+ "height": 0.8,
124
+ "subcircuit_id": "subcircuit_child_b"
125
+ }
126
+ ]
@@ -2,6 +2,7 @@ import { expect, test } from "bun:test"
2
2
  import type { AnyCircuitElement } from "circuit-json"
3
3
  import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map"
4
4
  import { convertCircuitJsonToInputProblem } from "lib/circuit-json/convert-circuit-json-to-input-problem"
5
+ import subcircuitConnectivityScopeCircuitJson from "./assets/subcircuit-connectivity-scope.json"
5
6
 
6
7
  const circuitJson = [
7
8
  {
@@ -98,3 +99,48 @@ test("circuit-json adapter rejects generated connectivity map keys", () => {
98
99
  }),
99
100
  ).toThrow(/subcircuit_connectivity_map_key/)
100
101
  })
102
+
103
+ test("subcircuit_id scopes repeated subcircuit connectivity keys", () => {
104
+ const inputProblem = convertCircuitJsonToInputProblem(
105
+ subcircuitConnectivityScopeCircuitJson as AnyCircuitElement[],
106
+ {
107
+ layer: "top",
108
+ subcircuit_id: "subcircuit_child_a",
109
+ subcircuit_connectivity_map_key: "net0",
110
+ pad_margin: 0.2,
111
+ trace_margin: 0.2,
112
+ },
113
+ )
114
+
115
+ const childAPad = inputProblem.pads.find(
116
+ (pad) => pad.padId === "pcb_smtpad_child_a_gnd",
117
+ )
118
+ const childBPad = inputProblem.pads.find(
119
+ (pad) => pad.padId === "pcb_smtpad_child_b_gnd",
120
+ )
121
+
122
+ expect(inputProblem.regionsForPour[0]?.connectivityKey).toBe(
123
+ "subcircuit:subcircuit_child_a:connectivity:net0",
124
+ )
125
+ expect(childAPad?.connectivityKey).toBe(
126
+ inputProblem.regionsForPour[0]?.connectivityKey,
127
+ )
128
+ expect(childBPad?.connectivityKey).not.toBe(
129
+ inputProblem.regionsForPour[0]?.connectivityKey,
130
+ )
131
+ })
132
+
133
+ test("parent subcircuit selection rejects ambiguous child connectivity keys", () => {
134
+ expect(() =>
135
+ convertCircuitJsonToInputProblem(
136
+ subcircuitConnectivityScopeCircuitJson as AnyCircuitElement[],
137
+ {
138
+ layer: "top",
139
+ subcircuit_id: "subcircuit_parent",
140
+ subcircuit_connectivity_map_key: "net0",
141
+ pad_margin: 0.2,
142
+ trace_margin: 0.2,
143
+ },
144
+ ),
145
+ ).toThrow(/multiple subcircuits/)
146
+ })
@@ -0,0 +1,223 @@
1
+ import { expect, test } from "bun:test"
2
+ import { Circuit } from "@tscircuit/core"
3
+ import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
4
+ import type {
5
+ AnyCircuitElement,
6
+ PcbCopperPourBRep,
7
+ SourceNet,
8
+ } from "circuit-json"
9
+ import { CopperPourPipelineSolver } from "lib/index"
10
+ import { convertCircuitJsonToInputProblem } from "lib/circuit-json/convert-circuit-json-to-input-problem"
11
+
12
+ const SubcircuitChild = () => (
13
+ <subcircuit
14
+ name="SubcircuitChild"
15
+ pcbX={2}
16
+ pcbY={0}
17
+ autorouter="sequential_trace"
18
+ >
19
+ <pinheader
20
+ name="J2"
21
+ pinCount={2}
22
+ footprint="pinrow2"
23
+ pcbX={0}
24
+ pcbY={0}
25
+ pinLabels={{ pin1: "B", pin2: "C" }}
26
+ showSilkscreenPinLabels
27
+ connections={{ C: "net.GND" }}
28
+ />
29
+ </subcircuit>
30
+ )
31
+
32
+ interface SubcircuitConnectivityReproProps {
33
+ includeParentToChildTrace: boolean
34
+ }
35
+
36
+ const SubcircuitConnectivityRepro = ({
37
+ includeParentToChildTrace,
38
+ }: SubcircuitConnectivityReproProps) => (
39
+ <board
40
+ name="SubcircuitParent"
41
+ width="10mm"
42
+ height="7mm"
43
+ autorouter="sequential_trace"
44
+ >
45
+ <pinheader
46
+ name="J1"
47
+ pinCount={1}
48
+ footprint="pinrow1"
49
+ pcbX={-3}
50
+ pcbY={0}
51
+ pinLabels={{ pin1: "A" }}
52
+ showSilkscreenPinLabels
53
+ connections={{ A: "net.GND" }}
54
+ />
55
+
56
+ <SubcircuitChild />
57
+
58
+ {includeParentToChildTrace && (
59
+ <trace from="J1.A" to=".SubcircuitChild > net.GND" />
60
+ )}
61
+
62
+ <copperpour
63
+ name="top_gnd_pour"
64
+ layer="top"
65
+ connectsTo="net.GND"
66
+ padMargin="0.25mm"
67
+ traceMargin="0.2mm"
68
+ />
69
+
70
+ <pcbnotetext
71
+ text={
72
+ includeParentToChildTrace
73
+ ? "Expected: copper pour connects to A and C, but clears around B."
74
+ : "Expected: copper pour connects to A, but clears around B and C."
75
+ }
76
+ pcbX={0}
77
+ pcbY={4.35}
78
+ fontSize="0.22mm"
79
+ anchorAlignment="center"
80
+ color="#ffffff"
81
+ />
82
+ <pcbnotetext
83
+ text={
84
+ includeParentToChildTrace
85
+ ? "A is parent net.GND; C is child net.GND reached through the parent-to-child net."
86
+ : "A is parent net.GND; C is child net.GND with no parent-to-child net."
87
+ }
88
+ pcbX={0}
89
+ pcbY={4.05}
90
+ fontSize="0.22mm"
91
+ anchorAlignment="center"
92
+ color="#ffffff"
93
+ />
94
+ <pcbnotetext
95
+ text="B is a separate child pin, so the pour should clear around it."
96
+ pcbX={0}
97
+ pcbY={3.75}
98
+ fontSize="0.22mm"
99
+ anchorAlignment="center"
100
+ color="#ffffff"
101
+ />
102
+ </board>
103
+ )
104
+
105
+ const renderCircuitJson = async (
106
+ props: SubcircuitConnectivityReproProps,
107
+ ): Promise<AnyCircuitElement[]> => {
108
+ const circuit = new Circuit()
109
+ const originalLog = console.log
110
+ const originalWarn = console.warn
111
+ console.log = () => {}
112
+ console.warn = () => {}
113
+ try {
114
+ circuit.add(<SubcircuitConnectivityRepro {...props} />)
115
+ await circuit.renderUntilSettled()
116
+ return circuit.getCircuitJson()
117
+ } finally {
118
+ console.log = originalLog
119
+ console.warn = originalWarn
120
+ }
121
+ }
122
+
123
+ const getPadCenterX = (pad: any) => {
124
+ if (typeof pad.x === "number") return pad.x
125
+ return (pad.bounds.minX + pad.bounds.maxX) / 2
126
+ }
127
+
128
+ const getPcbPadConnectivityKeys = (inputProblem: any) => {
129
+ const [aPad, bPad, cPad] = inputProblem.pads
130
+ .filter((pad: any) => pad.shape === "circle" || pad.shape === "rect")
131
+ .sort((a: any, b: any) => getPadCenterX(a) - getPadCenterX(b))
132
+
133
+ return {
134
+ a: aPad?.connectivityKey,
135
+ b: bPad?.connectivityKey,
136
+ c: cPad?.connectivityKey,
137
+ }
138
+ }
139
+
140
+ const runSubcircuitConnectivityCase = async ({
141
+ includeParentToChildTrace,
142
+ snapshotName,
143
+ expectedSourceNetConnectivityKeyCount,
144
+ expectChildGndConnectedToPour,
145
+ }: {
146
+ includeParentToChildTrace: boolean
147
+ snapshotName: string
148
+ expectedSourceNetConnectivityKeyCount: number
149
+ expectChildGndConnectedToPour: boolean
150
+ }) => {
151
+ const circuitJson = await renderCircuitJson({ includeParentToChildTrace })
152
+ const gndSourceNets = circuitJson.filter(
153
+ (element): element is SourceNet =>
154
+ element.type === "source_net" && element.name === "GND",
155
+ )
156
+
157
+ expect(gndSourceNets).toHaveLength(2)
158
+ expect(
159
+ new Set(gndSourceNets.map((net) => net.subcircuit_connectivity_map_key))
160
+ .size,
161
+ ).toBe(expectedSourceNetConnectivityKeyCount)
162
+ expect(new Set(gndSourceNets.map((net) => net.subcircuit_id)).size).toBe(2)
163
+
164
+ const parentGndSourceNet = gndSourceNets.find((net) =>
165
+ net.subcircuit_connectivity_map_key?.includes("SubcircuitParent"),
166
+ )
167
+ expect(parentGndSourceNet).toBeDefined()
168
+
169
+ const inputProblem = convertCircuitJsonToInputProblem(circuitJson, {
170
+ layer: "top",
171
+ source_net_id: parentGndSourceNet!.source_net_id,
172
+ pad_margin: 0.25,
173
+ trace_margin: 0.2,
174
+ })
175
+
176
+ const pourConnectivityKey = inputProblem.regionsForPour[0]?.connectivityKey
177
+ const padConnectivityKeys = getPcbPadConnectivityKeys(inputProblem)
178
+
179
+ expect(padConnectivityKeys.a).toBe(pourConnectivityKey)
180
+ expect(padConnectivityKeys.b).not.toBe(pourConnectivityKey)
181
+ if (expectChildGndConnectedToPour) {
182
+ expect(padConnectivityKeys.c).toBe(pourConnectivityKey)
183
+ } else {
184
+ expect(padConnectivityKeys.c).not.toBe(pourConnectivityKey)
185
+ }
186
+
187
+ const output = new CopperPourPipelineSolver(inputProblem).getOutput()
188
+ const copperPours: PcbCopperPourBRep[] = output.brep_shapes.map(
189
+ (brep_shape, i) => ({
190
+ type: "pcb_copper_pour",
191
+ shape: "brep",
192
+ pcb_copper_pour_id: `pcb_copper_pour_tsx_subcircuit_${i}`,
193
+ layer: "top",
194
+ source_net_id: parentGndSourceNet!.source_net_id,
195
+ brep_shape,
196
+ covered_with_solder_mask: true,
197
+ }),
198
+ )
199
+ const svg = convertCircuitJsonToPcbSvg([
200
+ ...circuitJson.filter((element) => element.type !== "pcb_copper_pour"),
201
+ ...copperPours,
202
+ ] as any)
203
+
204
+ await expect(svg).toMatchSvgSnapshot(import.meta.path, snapshotName)
205
+ }
206
+
207
+ test("tsx subcircuit connectivity 01 connects child net through parent-to-child trace", async () => {
208
+ await runSubcircuitConnectivityCase({
209
+ includeParentToChildTrace: true,
210
+ snapshotName: "tsx-subcircuit-connectivity01",
211
+ expectedSourceNetConnectivityKeyCount: 1,
212
+ expectChildGndConnectedToPour: true,
213
+ })
214
+ })
215
+
216
+ test("tsx subcircuit connectivity 02 keeps child net separate without parent-to-child trace", async () => {
217
+ await runSubcircuitConnectivityCase({
218
+ includeParentToChildTrace: false,
219
+ snapshotName: "tsx-subcircuit-connectivity02",
220
+ expectedSourceNetConnectivityKeyCount: 2,
221
+ expectChildGndConnectedToPour: false,
222
+ })
223
+ })