@tscircuit/schematic-trace-solver 0.0.4 → 0.0.5

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.
@@ -2,5 +2,6 @@
2
2
  "$schema": "http://json.schemastore.org/cosmos-config",
3
3
  "plugins": ["react-cosmos-plugin-vite"],
4
4
  "fixtureFileSuffix": "page",
5
- "decoratorFile": "cosmos.decorator.tsx"
5
+ "decoratorFile": "cosmos.decorator.tsx",
6
+ "port": 5020
6
7
  }
package/dist/index.d.ts CHANGED
@@ -85,6 +85,7 @@ interface InputProblem {
85
85
  directConnections: Array<InputDirectConnection>;
86
86
  netConnections: Array<InputNetConnection>;
87
87
  availableNetLabelOrientations: Record<NetId, FacingDirection[]>;
88
+ maxMspPairDistance?: number;
88
89
  _chipObstacleSpatialIndex?: ChipObstacleSpatialIndex;
89
90
  }
90
91
 
@@ -107,6 +108,7 @@ declare class MspConnectionPairSolver extends BaseSolver {
107
108
  globalConnMap: ConnectivityMap;
108
109
  queuedDcNetIds: string[];
109
110
  chipMap: Record<string, InputChip>;
111
+ maxMspPairDistance: number;
110
112
  pinMap: Record<string, InputPin & {
111
113
  chipId: string;
112
114
  }>;
@@ -339,7 +341,8 @@ declare class SingleNetLabelPlacementSolver extends BaseSolver {
339
341
  type OverlappingSameNetTraceGroup = {
340
342
  globalConnNetId: string;
341
343
  netId?: string;
342
- overlappingTraces: SolvedTracePath;
344
+ overlappingTraces?: SolvedTracePath;
345
+ portOnlyPinId?: string;
343
346
  };
344
347
  interface NetLabelPlacement {
345
348
  globalConnNetId: string;
package/dist/index.js CHANGED
@@ -5274,8 +5274,9 @@ var getConnectivityMapsFromInputProblem = (inputProblem) => {
5274
5274
  };
5275
5275
 
5276
5276
  // lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts
5277
- function getOrthogonalMinimumSpanningTree(pins) {
5277
+ function getOrthogonalMinimumSpanningTree(pins, opts = {}) {
5278
5278
  const n = pins.length;
5279
+ const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY;
5279
5280
  if (n <= 1) return [];
5280
5281
  {
5281
5282
  const seen = /* @__PURE__ */ new Set();
@@ -5316,7 +5317,8 @@ function getOrthogonalMinimumSpanningTree(pins) {
5316
5317
  }
5317
5318
  for (let v = 0; v < n; v++) {
5318
5319
  if (!inTree[v]) {
5319
- const d = manhattan(pins[u], pins[v]);
5320
+ const d0 = manhattan(pins[u], pins[v]);
5321
+ const d = d0 > maxDistance ? Number.POSITIVE_INFINITY : d0;
5320
5322
  if (d < bestDist[v] || d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId) {
5321
5323
  bestDist[v] = d;
5322
5324
  parent[v] = u;
@@ -5446,11 +5448,13 @@ var MspConnectionPairSolver = class extends BaseSolver {
5446
5448
  globalConnMap;
5447
5449
  queuedDcNetIds;
5448
5450
  chipMap;
5451
+ maxMspPairDistance;
5449
5452
  pinMap;
5450
5453
  userNetIdByPinId;
5451
5454
  constructor({ inputProblem }) {
5452
5455
  super();
5453
5456
  this.inputProblem = inputProblem;
5457
+ this.maxMspPairDistance = inputProblem.maxMspPairDistance ?? 1;
5454
5458
  const { directConnMap, netConnMap } = getConnectivityMapsFromInputProblem(inputProblem);
5455
5459
  this.dcConnMap = directConnMap;
5456
5460
  this.globalConnMap = netConnMap;
@@ -5508,7 +5512,8 @@ var MspConnectionPairSolver = class extends BaseSolver {
5508
5512
  return;
5509
5513
  }
5510
5514
  const msp = getOrthogonalMinimumSpanningTree(
5511
- directlyConnectedPins.map((p) => this.pinMap[p])
5515
+ directlyConnectedPins.map((p) => this.pinMap[p]),
5516
+ { maxDistance: this.maxMspPairDistance }
5512
5517
  );
5513
5518
  for (const [pin1, pin2] of msp) {
5514
5519
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1);
@@ -7232,6 +7237,92 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7232
7237
  this.solved = true;
7233
7238
  return;
7234
7239
  }
7240
+ if (this.overlappingSameNetTraceGroup.portOnlyPinId) {
7241
+ const pinId = this.overlappingSameNetTraceGroup.portOnlyPinId;
7242
+ let pin = null;
7243
+ for (const chip of this.inputProblem.chips) {
7244
+ const p = chip.pins.find((pp) => pp.pinId === pinId);
7245
+ if (p) {
7246
+ pin = { x: p.x, y: p.y };
7247
+ break;
7248
+ }
7249
+ }
7250
+ if (!pin) {
7251
+ this.failed = true;
7252
+ this.error = `Port-only pin not found: ${pinId}`;
7253
+ return;
7254
+ }
7255
+ const orientations2 = this.availableOrientations.length > 0 ? this.availableOrientations : ["x+", "x-", "y+", "y-"];
7256
+ const anchor = { x: pin.x, y: pin.y };
7257
+ const outwardOf = (o) => o === "x+" ? { x: 1, y: 0 } : o === "x-" ? { x: -1, y: 0 } : o === "y+" ? { x: 0, y: 1 } : { x: 0, y: -1 };
7258
+ for (const orientation of orientations2) {
7259
+ const { width, height } = this.getDimsForOrientation(orientation);
7260
+ const baseCenter = this.getCenterFromAnchor(
7261
+ anchor,
7262
+ orientation,
7263
+ width,
7264
+ height
7265
+ );
7266
+ const outward = outwardOf(orientation);
7267
+ const offset = 1e-3;
7268
+ const center = {
7269
+ x: baseCenter.x + outward.x * offset,
7270
+ y: baseCenter.y + outward.y * offset
7271
+ };
7272
+ const bounds = this.getRectBounds(center, width, height);
7273
+ const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
7274
+ if (chips.length > 0) {
7275
+ this.testedCandidates.push({
7276
+ center,
7277
+ width,
7278
+ height,
7279
+ bounds,
7280
+ anchor,
7281
+ orientation,
7282
+ status: "chip-collision",
7283
+ hostSegIndex: -1
7284
+ });
7285
+ continue;
7286
+ }
7287
+ if (this.rectIntersectsAnyTrace(bounds, "", -1)) {
7288
+ this.testedCandidates.push({
7289
+ center,
7290
+ width,
7291
+ height,
7292
+ bounds,
7293
+ anchor,
7294
+ orientation,
7295
+ status: "trace-collision",
7296
+ hostSegIndex: -1
7297
+ });
7298
+ continue;
7299
+ }
7300
+ this.testedCandidates.push({
7301
+ center,
7302
+ width,
7303
+ height,
7304
+ bounds,
7305
+ anchor,
7306
+ orientation,
7307
+ status: "ok",
7308
+ hostSegIndex: -1
7309
+ });
7310
+ this.netLabelPlacement = {
7311
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
7312
+ dcConnNetId: void 0,
7313
+ orientation,
7314
+ anchorPoint: anchor,
7315
+ width,
7316
+ height,
7317
+ center
7318
+ };
7319
+ this.solved = true;
7320
+ return;
7321
+ }
7322
+ this.failed = true;
7323
+ this.error = "Could not place net label at port without collisions";
7324
+ return;
7325
+ }
7235
7326
  const groupId = this.overlappingSameNetTraceGroup.globalConnNetId;
7236
7327
  const chipsById = Object.fromEntries(
7237
7328
  this.inputProblem.chips.map((c) => [c.chipId, c])
@@ -7266,8 +7357,13 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7266
7357
  (t) => t.pins[0].chipId === largestChipId || t.pins[1].chipId === largestChipId
7267
7358
  );
7268
7359
  let host = hostCandidates.length > 0 ? hostCandidates.reduce((a, b) => lengthOf(a) >= lengthOf(b) ? a : b) : this.overlappingSameNetTraceGroup.overlappingTraces;
7360
+ if (!host) {
7361
+ this.failed = true;
7362
+ this.error = "No host trace found for net label placement";
7363
+ return;
7364
+ }
7269
7365
  let pts = host.tracePath.slice();
7270
- if (largestChipId) {
7366
+ if (largestChipId && host) {
7271
7367
  const largePin = host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1];
7272
7368
  const d0 = Math.abs(pts[0].x - largePin.x) + Math.abs(pts[0].y - largePin.y);
7273
7369
  const dL = Math.abs(pts[pts.length - 1].x - largePin.x) + Math.abs(pts[pts.length - 1].y - largePin.y);
@@ -7406,7 +7502,7 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7406
7502
  const groupFill = getColorFromString(groupId, 0.5);
7407
7503
  for (const trace of Object.values(this.inputTraceMap)) {
7408
7504
  if (trace.globalConnNetId !== groupId) continue;
7409
- const isHost = trace.mspPairId === host.mspPairId;
7505
+ const isHost = host ? trace.mspPairId === host.mspPairId : false;
7410
7506
  graphics.lines.push({
7411
7507
  points: trace.tracePath,
7412
7508
  strokeColor: isHost ? groupStroke : groupFill,
@@ -7475,32 +7571,100 @@ var NetLabelPlacementSolver = class extends BaseSolver {
7475
7571
  if (!byGlobal[key]) byGlobal[key] = [];
7476
7572
  byGlobal[key].push(trace);
7477
7573
  }
7574
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
7575
+ this.inputProblem
7576
+ );
7577
+ const userNetIdByPinId = {};
7578
+ for (const dc of this.inputProblem.directConnections) {
7579
+ if (dc.netId) {
7580
+ const [a, b] = dc.pinIds;
7581
+ userNetIdByPinId[a] = dc.netId;
7582
+ userNetIdByPinId[b] = dc.netId;
7583
+ }
7584
+ }
7585
+ for (const nc of this.inputProblem.netConnections) {
7586
+ for (const pid of nc.pinIds) {
7587
+ userNetIdByPinId[pid] = nc.netId;
7588
+ }
7589
+ }
7478
7590
  const groups = [];
7479
- for (const [globalConnNetId, traces] of Object.entries(byGlobal)) {
7480
- if (traces.length === 0) continue;
7481
- const lengthOf = (path) => {
7482
- let sum = 0;
7483
- const pts = path.tracePath;
7484
- for (let i = 0; i < pts.length - 1; i++) {
7485
- sum += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y);
7486
- }
7487
- return sum;
7488
- };
7489
- let rep = traces[0];
7490
- let repLen = lengthOf(rep);
7491
- for (let i = 1; i < traces.length; i++) {
7492
- const len = lengthOf(traces[i]);
7493
- if (len > repLen) {
7494
- rep = traces[i];
7495
- repLen = len;
7591
+ for (const globalConnNetId of Object.keys(netConnMap.netMap)) {
7592
+ const pinsInNet = netConnMap.getIdsConnectedToNet(
7593
+ globalConnNetId
7594
+ );
7595
+ const adj = {};
7596
+ for (const pid of pinsInNet) adj[pid] = /* @__PURE__ */ new Set();
7597
+ for (const t of byGlobal[globalConnNetId] ?? []) {
7598
+ const a = t.pins[0].pinId;
7599
+ const b = t.pins[1].pinId;
7600
+ if (adj[a] && adj[b]) {
7601
+ adj[a].add(b);
7602
+ adj[b].add(a);
7603
+ }
7604
+ }
7605
+ const visited = /* @__PURE__ */ new Set();
7606
+ for (const pid of pinsInNet) {
7607
+ if (visited.has(pid)) continue;
7608
+ const stack = [pid];
7609
+ const component = /* @__PURE__ */ new Set();
7610
+ visited.add(pid);
7611
+ while (stack.length > 0) {
7612
+ const u = stack.pop();
7613
+ component.add(u);
7614
+ for (const v of adj[u] ?? []) {
7615
+ if (!visited.has(v)) {
7616
+ visited.add(v);
7617
+ stack.push(v);
7618
+ }
7619
+ }
7620
+ }
7621
+ const compTraces = (byGlobal[globalConnNetId] ?? []).filter(
7622
+ (t) => component.has(t.pins[0].pinId) && component.has(t.pins[1].pinId)
7623
+ );
7624
+ if (compTraces.length > 0) {
7625
+ const lengthOf = (path) => {
7626
+ let sum = 0;
7627
+ const pts = path.tracePath;
7628
+ for (let i = 0; i < pts.length - 1; i++) {
7629
+ sum += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y);
7630
+ }
7631
+ return sum;
7632
+ };
7633
+ let rep = compTraces[0];
7634
+ let repLen = lengthOf(rep);
7635
+ for (let i = 1; i < compTraces.length; i++) {
7636
+ const len = lengthOf(compTraces[i]);
7637
+ if (len > repLen) {
7638
+ rep = compTraces[i];
7639
+ repLen = len;
7640
+ }
7641
+ }
7642
+ let userNetId = compTraces.find((t) => t.userNetId != null)?.userNetId;
7643
+ if (!userNetId) {
7644
+ for (const p of component) {
7645
+ if (userNetIdByPinId[p]) {
7646
+ userNetId = userNetIdByPinId[p];
7647
+ break;
7648
+ }
7649
+ }
7650
+ }
7651
+ groups.push({
7652
+ globalConnNetId,
7653
+ netId: userNetId,
7654
+ overlappingTraces: rep
7655
+ });
7656
+ } else {
7657
+ for (const p of component) {
7658
+ const userNetId = userNetIdByPinId[p];
7659
+ if (!userNetId) continue;
7660
+ groups.push({
7661
+ globalConnNetId,
7662
+ netId: userNetId,
7663
+ portOnlyPinId: p
7664
+ });
7665
+ }
7496
7666
  }
7497
7667
  }
7498
- const userNetId = traces.find((t) => t.userNetId != null)?.userNetId;
7499
- groups.push({
7500
- globalConnNetId,
7501
- netId: userNetId,
7502
- overlappingTraces: rep
7503
- });
7504
7668
  }
7505
7669
  return groups;
7506
7670
  }
@@ -25,6 +25,7 @@ export class MspConnectionPairSolver extends BaseSolver {
25
25
  globalConnMap: ConnectivityMap
26
26
  queuedDcNetIds: string[]
27
27
  chipMap: Record<string, InputChip>
28
+ maxMspPairDistance: number
28
29
 
29
30
  pinMap: Record<string, InputPin & { chipId: string }>
30
31
  userNetIdByPinId: Record<string, string | undefined>
@@ -33,6 +34,7 @@ export class MspConnectionPairSolver extends BaseSolver {
33
34
  super()
34
35
 
35
36
  this.inputProblem = inputProblem
37
+ this.maxMspPairDistance = inputProblem.maxMspPairDistance ?? 1
36
38
 
37
39
  const { directConnMap, netConnMap } =
38
40
  getConnectivityMapsFromInputProblem(inputProblem)
@@ -112,6 +114,7 @@ export class MspConnectionPairSolver extends BaseSolver {
112
114
 
113
115
  const msp = getOrthogonalMinimumSpanningTree(
114
116
  directlyConnectedPins.map((p) => this.pinMap[p]!),
117
+ { maxDistance: this.maxMspPairDistance },
115
118
  )
116
119
 
117
120
  for (const [pin1, pin2] of msp) {
@@ -18,8 +18,10 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
18
18
  */
19
19
  export function getOrthogonalMinimumSpanningTree(
20
20
  pins: InputPin[],
21
+ opts: { maxDistance?: number } = {},
21
22
  ): Array<[PinId, PinId]> {
22
23
  const n = pins.length
24
+ const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY
23
25
  if (n <= 1) return []
24
26
 
25
27
  // Quick validation (optional; remove if hot path)
@@ -79,7 +81,8 @@ export function getOrthogonalMinimumSpanningTree(
79
81
  // Relax edges from u to all v not yet in the tree
80
82
  for (let v = 0; v < n; v++) {
81
83
  if (!inTree[v]) {
82
- const d = manhattan(pins[u], pins[v])
84
+ const d0 = manhattan(pins[u], pins[v])
85
+ const d = d0 > maxDistance ? Number.POSITIVE_INFINITY : d0
83
86
  if (
84
87
  d < bestDist[v] ||
85
88
  (d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId)
@@ -8,6 +8,7 @@ import type { Point } from "@tscircuit/math-utils"
8
8
  import type { GraphicsObject } from "graphics-debug"
9
9
  import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
10
10
  import { getColorFromString } from "lib/utils/getColorFromString"
11
+ import { getConnectivityMapsFromInputProblem } from "../MspConnectionPairSolver/getConnectivityMapFromInputProblem"
11
12
 
12
13
  /**
13
14
  * A group of traces that have at least one overlapping segment and
@@ -16,7 +17,8 @@ import { getColorFromString } from "lib/utils/getColorFromString"
16
17
  export type OverlappingSameNetTraceGroup = {
17
18
  globalConnNetId: string
18
19
  netId?: string
19
- overlappingTraces: SolvedTracePath
20
+ overlappingTraces?: SolvedTracePath
21
+ portOnlyPinId?: string
20
22
  }
21
23
 
22
24
  export interface NetLabelPlacement {
@@ -76,7 +78,7 @@ export class NetLabelPlacementSolver extends BaseSolver {
76
78
  }
77
79
 
78
80
  computeOverlappingSameNetTraceGroups(): Array<OverlappingSameNetTraceGroup> {
79
- // Group traces by their global connectivity net id.
81
+ // Group existing traces by their global connectivity net id.
80
82
  const byGlobal: Record<string, Array<SolvedTracePath>> = {}
81
83
  for (const trace of Object.values(this.inputTraceMap)) {
82
84
  const key = trace.globalConnNetId
@@ -84,35 +86,120 @@ export class NetLabelPlacementSolver extends BaseSolver {
84
86
  byGlobal[key].push(trace)
85
87
  }
86
88
 
87
- // For each group, pick a representative path (longest by L1 length).
89
+ // Build global connectivity from input so we also consider pins with no traces
90
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
91
+ this.inputProblem,
92
+ )
93
+
94
+ // Map pins to user-provided netIds (if any)
95
+ const userNetIdByPinId: Record<string, string | undefined> = {}
96
+ for (const dc of this.inputProblem.directConnections) {
97
+ if (dc.netId) {
98
+ const [a, b] = dc.pinIds
99
+ userNetIdByPinId[a] = dc.netId
100
+ userNetIdByPinId[b] = dc.netId
101
+ }
102
+ }
103
+ for (const nc of this.inputProblem.netConnections) {
104
+ for (const pid of nc.pinIds) {
105
+ userNetIdByPinId[pid] = nc.netId
106
+ }
107
+ }
108
+
88
109
  const groups: Array<OverlappingSameNetTraceGroup> = []
89
- for (const [globalConnNetId, traces] of Object.entries(byGlobal)) {
90
- if (traces.length === 0) continue
91
- const lengthOf = (path: SolvedTracePath) => {
92
- let sum = 0
93
- const pts = path.tracePath
94
- for (let i = 0; i < pts.length - 1; i++) {
95
- sum +=
96
- Math.abs(pts[i + 1]!.x - pts[i]!.x) +
97
- Math.abs(pts[i + 1]!.y - pts[i]!.y)
110
+
111
+ // Consider every global connectivity net id
112
+ for (const globalConnNetId of Object.keys((netConnMap as any).netMap)) {
113
+ const pinsInNet = netConnMap.getIdsConnectedToNet(
114
+ globalConnNetId,
115
+ ) as string[]
116
+
117
+ // Build adjacency from solved traces (edges)
118
+ const adj: Record<string, Set<string>> = {}
119
+ for (const pid of pinsInNet) adj[pid] = new Set()
120
+ for (const t of byGlobal[globalConnNetId] ?? []) {
121
+ const a = t.pins[0].pinId
122
+ const b = t.pins[1].pinId
123
+ if (adj[a] && adj[b]) {
124
+ adj[a].add(b)
125
+ adj[b].add(a)
98
126
  }
99
- return sum
100
127
  }
101
- let rep = traces[0]!
102
- let repLen = lengthOf(rep)
103
- for (let i = 1; i < traces.length; i++) {
104
- const len = lengthOf(traces[i]!)
105
- if (len > repLen) {
106
- rep = traces[i]!
107
- repLen = len
128
+
129
+ // Find connected components based on trace edges
130
+ const visited = new Set<string>()
131
+ for (const pid of pinsInNet) {
132
+ if (visited.has(pid)) continue
133
+ const stack = [pid]
134
+ const component = new Set<string>()
135
+ visited.add(pid)
136
+ while (stack.length > 0) {
137
+ const u = stack.pop()!
138
+ component.add(u)
139
+ for (const v of adj[u] ?? []) {
140
+ if (!visited.has(v)) {
141
+ visited.add(v)
142
+ stack.push(v)
143
+ }
144
+ }
145
+ }
146
+
147
+ // Collect traces fully inside this component
148
+ const compTraces = (byGlobal[globalConnNetId] ?? []).filter(
149
+ (t) =>
150
+ component.has(t.pins[0].pinId) && component.has(t.pins[1].pinId),
151
+ )
152
+
153
+ if (compTraces.length > 0) {
154
+ // Choose a representative trace (longest by L1 length)
155
+ const lengthOf = (path: SolvedTracePath) => {
156
+ let sum = 0
157
+ const pts = path.tracePath
158
+ for (let i = 0; i < pts.length - 1; i++) {
159
+ sum +=
160
+ Math.abs(pts[i + 1]!.x - pts[i]!.x) +
161
+ Math.abs(pts[i + 1]!.y - pts[i]!.y)
162
+ }
163
+ return sum
164
+ }
165
+ let rep = compTraces[0]!
166
+ let repLen = lengthOf(rep)
167
+ for (let i = 1; i < compTraces.length; i++) {
168
+ const len = lengthOf(compTraces[i]!)
169
+ if (len > repLen) {
170
+ rep = compTraces[i]!
171
+ repLen = len
172
+ }
173
+ }
174
+
175
+ let userNetId = compTraces.find((t) => t.userNetId != null)?.userNetId
176
+ if (!userNetId) {
177
+ for (const p of component) {
178
+ if (userNetIdByPinId[p]) {
179
+ userNetId = userNetIdByPinId[p]
180
+ break
181
+ }
182
+ }
183
+ }
184
+
185
+ groups.push({
186
+ globalConnNetId,
187
+ netId: userNetId,
188
+ overlappingTraces: rep,
189
+ })
190
+ } else {
191
+ // No traces in this component: place label at each pin that has a user net id
192
+ for (const p of component) {
193
+ const userNetId = userNetIdByPinId[p]
194
+ if (!userNetId) continue
195
+ groups.push({
196
+ globalConnNetId,
197
+ netId: userNetId,
198
+ portOnlyPinId: p,
199
+ })
200
+ }
108
201
  }
109
202
  }
110
- const userNetId = traces.find((t) => t.userNetId != null)?.userNetId
111
- groups.push({
112
- globalConnNetId,
113
- netId: userNetId,
114
- overlappingTraces: rep,
115
- })
116
203
  }
117
204
 
118
205
  return groups
@@ -172,6 +172,119 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
172
172
  return
173
173
  }
174
174
 
175
+ // Handle port-only island (no traces) by placing a label at the port
176
+ if (this.overlappingSameNetTraceGroup.portOnlyPinId) {
177
+ const pinId = this.overlappingSameNetTraceGroup.portOnlyPinId
178
+ // Find pin coordinates
179
+ let pin: { x: number; y: number } | null = null
180
+ for (const chip of this.inputProblem.chips) {
181
+ const p = chip.pins.find((pp) => pp.pinId === pinId)
182
+ if (p) {
183
+ pin = { x: p.x, y: p.y }
184
+ break
185
+ }
186
+ }
187
+ if (!pin) {
188
+ this.failed = true
189
+ this.error = `Port-only pin not found: ${pinId}`
190
+ return
191
+ }
192
+
193
+ const orientations =
194
+ this.availableOrientations.length > 0
195
+ ? this.availableOrientations
196
+ : (["x+", "x-", "y+", "y-"] as FacingDirection[])
197
+
198
+ const anchor = { x: pin.x, y: pin.y }
199
+ const outwardOf = (o: FacingDirection) =>
200
+ o === "x+"
201
+ ? { x: 1, y: 0 }
202
+ : o === "x-"
203
+ ? { x: -1, y: 0 }
204
+ : o === "y+"
205
+ ? { x: 0, y: 1 }
206
+ : { x: 0, y: -1 }
207
+
208
+ for (const orientation of orientations) {
209
+ const { width, height } = this.getDimsForOrientation(orientation)
210
+ // Place label fully outside the chip: shift center slightly outward
211
+ const baseCenter = this.getCenterFromAnchor(
212
+ anchor,
213
+ orientation,
214
+ width,
215
+ height,
216
+ )
217
+ const outward = outwardOf(orientation)
218
+ const offset = 1e-3
219
+ const center = {
220
+ x: baseCenter.x + outward.x * offset,
221
+ y: baseCenter.y + outward.y * offset,
222
+ }
223
+ const bounds = this.getRectBounds(center, width, height)
224
+
225
+ // Chip collision check
226
+ const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
227
+ if (chips.length > 0) {
228
+ this.testedCandidates.push({
229
+ center,
230
+ width,
231
+ height,
232
+ bounds,
233
+ anchor,
234
+ orientation,
235
+ status: "chip-collision",
236
+ hostSegIndex: -1,
237
+ })
238
+ continue
239
+ }
240
+
241
+ // Trace collision check
242
+ if (
243
+ this.rectIntersectsAnyTrace(bounds, "" as MspConnectionPairId, -1)
244
+ ) {
245
+ this.testedCandidates.push({
246
+ center,
247
+ width,
248
+ height,
249
+ bounds,
250
+ anchor,
251
+ orientation,
252
+ status: "trace-collision",
253
+ hostSegIndex: -1,
254
+ })
255
+ continue
256
+ }
257
+
258
+ // Found a valid placement
259
+ this.testedCandidates.push({
260
+ center,
261
+ width,
262
+ height,
263
+ bounds,
264
+ anchor,
265
+ orientation,
266
+ status: "ok",
267
+ hostSegIndex: -1,
268
+ })
269
+
270
+ this.netLabelPlacement = {
271
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
272
+ dcConnNetId: undefined,
273
+ orientation,
274
+ anchorPoint: anchor,
275
+ width,
276
+ height,
277
+ center,
278
+ }
279
+ this.solved = true
280
+ return
281
+ }
282
+
283
+ this.failed = true
284
+ this.error = "Could not place net label at port without collisions"
285
+ return
286
+ }
287
+
175
288
  // Prefer starting from the trace connected to the "largest" chip (most pins)
176
289
  const groupId = this.overlappingSameNetTraceGroup.globalConnNetId
177
290
  const chipsById: Record<string, InputChip> = Object.fromEntries(
@@ -218,9 +331,15 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
218
331
  ? hostCandidates.reduce((a, b) => (lengthOf(a) >= lengthOf(b) ? a : b))
219
332
  : this.overlappingSameNetTraceGroup.overlappingTraces
220
333
 
334
+ if (!host) {
335
+ this.failed = true
336
+ this.error = "No host trace found for net label placement"
337
+ return
338
+ }
339
+
221
340
  // Ensure we traverse the host path starting at the segment attached to the largest chip's pin
222
341
  let pts = host.tracePath.slice()
223
- if (largestChipId) {
342
+ if (largestChipId && host) {
224
343
  const largePin =
225
344
  host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1]
226
345
  const d0 =
@@ -312,7 +431,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
312
431
  }
313
432
 
314
433
  // Trace collision check (ignore the host segment)
315
- if (this.rectIntersectsAnyTrace(bounds, host.mspPairId, si)) {
434
+ if (this.rectIntersectsAnyTrace(bounds, host!.mspPairId, si)) {
316
435
  this.testedCandidates.push({
317
436
  center: testCenter,
318
437
  width,
@@ -340,7 +459,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
340
459
 
341
460
  this.netLabelPlacement = {
342
461
  globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
343
- dcConnNetId: host.dcConnNetId,
462
+ dcConnNetId: host!.dcConnNetId,
344
463
  orientation,
345
464
  anchorPoint: anchor,
346
465
  width,
@@ -411,7 +530,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
411
530
 
412
531
  for (const trace of Object.values(this.inputTraceMap)) {
413
532
  if (trace.globalConnNetId !== groupId) continue
414
- const isHost = trace.mspPairId === host.mspPairId
533
+ const isHost = host ? trace.mspPairId === host.mspPairId : false
415
534
  graphics.lines!.push({
416
535
  points: trace.tracePath,
417
536
  strokeColor: isHost ? groupStroke : groupFill,
@@ -35,6 +35,7 @@ export interface InputProblem {
35
35
  netConnections: Array<InputNetConnection>
36
36
 
37
37
  availableNetLabelOrientations: Record<NetId, FacingDirection[]>
38
+ maxMspPairDistance?: number
38
39
 
39
40
  _chipObstacleSpatialIndex?: ChipObstacleSpatialIndex
40
41
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.4",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -99,6 +99,7 @@ const inputProblem: InputProblem = {
99
99
  EN: ["x+", "x-"],
100
100
  GND: ["y+", "y-"],
101
101
  },
102
+ maxMspPairDistance: 2,
102
103
  }
103
104
 
104
105
  export default () => <PipelineDebugger inputProblem={inputProblem} />
@@ -171,6 +171,7 @@ const inputProblem: InputProblem = {
171
171
  pinIds: ["C5.1", "U1.5"],
172
172
  },
173
173
  ],
174
+ maxMspPairDistance: 2,
174
175
  availableNetLabelOrientations: {
175
176
  VSYS: ["y+"],
176
177
  GND: ["y-"],
package/vite.config.ts CHANGED
@@ -9,4 +9,7 @@ export default defineConfig({
9
9
  tests: path.resolve(__dirname, "tests"),
10
10
  },
11
11
  },
12
+ server: {
13
+ port: 5020,
14
+ },
12
15
  })