@tscircuit/schematic-trace-solver 0.0.98 → 0.0.100

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.
Files changed (26) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +335 -225
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +43 -1
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
  6. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +13 -3
  7. package/package.json +1 -1
  8. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
  9. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
  10. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
  11. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
  12. package/tests/examples/__snapshots__/example09.snap.svg +9 -13
  13. package/tests/examples/__snapshots__/example21.snap.svg +30 -32
  14. package/tests/examples/__snapshots__/example29.snap.svg +13 -19
  15. package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
  16. package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +65 -0
  17. package/tests/repros/__snapshots__/repro5-escape-padded-text-obstacles.snap.svg +74 -0
  18. package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
  19. package/tests/repros/assets/repro47-endpoint-obstacle-detour.input.json +133 -0
  20. package/tests/repros/assets/repro5-escape-padded-text-obstacles.input.json +202 -0
  21. package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
  22. package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +19 -0
  23. package/tests/repros/repro5-escape-padded-text-obstacles.test.ts +17 -0
  24. package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
  25. package/tests/solvers/SchematicTraceSingleLineSolver2/candidate-mids-from-set.test.ts +39 -0
  26. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +38 -0
package/dist/index.js CHANGED
@@ -76,80 +76,39 @@ var BaseSolver = class {
76
76
  };
77
77
 
78
78
  // lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts
79
- import "connectivity-map";
79
+ import { distance } from "@tscircuit/math-utils";
80
80
 
81
- // lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts
82
- import { ConnectivityMap } from "connectivity-map";
83
- var getConnectivityMapsFromInputProblem = (inputProblem) => {
84
- const directConnMap = new ConnectivityMap({});
85
- for (const directConn of inputProblem.directConnections) {
86
- directConnMap.addConnections([
87
- directConn.netId ? [directConn.netId, ...directConn.pinIds] : directConn.pinIds
88
- ]);
89
- }
90
- const netConnMap = new ConnectivityMap(directConnMap.netMap);
91
- for (const netConn of inputProblem.netConnections) {
92
- netConnMap.addConnections([[netConn.netId, ...netConn.pinIds]]);
93
- }
94
- return { directConnMap, netConnMap };
81
+ // lib/utils/getColorFromString.ts
82
+ var getColorFromString = (string, alpha = 1) => {
83
+ const hash = string.split("").reduce((acc, char) => {
84
+ return acc * 31 + char.charCodeAt(0);
85
+ }, 0);
86
+ return `hsl(${hash % 360}, 100%, 50%, ${alpha})`;
95
87
  };
96
88
 
97
- // lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts
98
- function getOrthogonalMinimumSpanningTree(pins, opts = {}) {
99
- const n = pins.length;
100
- const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY;
101
- if (n <= 1) return [];
102
- {
103
- const seen = /* @__PURE__ */ new Set();
104
- for (const p of pins) {
105
- if (seen.has(p.pinId)) {
106
- throw new Error(`Duplicate pinId detected: "${p.pinId}"`);
107
- }
108
- seen.add(p.pinId);
109
- }
110
- }
111
- const manhattan = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
112
- const inTree = new Array(n).fill(false);
113
- const bestDist = new Array(n).fill(Number.POSITIVE_INFINITY);
114
- const parent = new Array(n).fill(-1);
115
- let startIndex = 0;
116
- for (let i = 1; i < n; i++) {
117
- if (pins[i].pinId < pins[startIndex].pinId) startIndex = i;
89
+ // lib/utils/arePinsInDifferentSchematicSections.ts
90
+ var getSectionNameForPin = (sectionByChipId, sectionByPinId, pin) => {
91
+ if (pin.chipId) {
92
+ const chipSection = sectionByChipId.get(pin.chipId);
93
+ if (chipSection) return chipSection;
118
94
  }
119
- bestDist[startIndex] = 0;
120
- const edges = [];
121
- for (let iter = 0; iter < n; iter++) {
122
- let u = -1;
123
- let best = Number.POSITIVE_INFINITY;
124
- let bestId = "";
125
- for (let i = 0; i < n; i++) {
126
- if (!inTree[i]) {
127
- const d = bestDist[i];
128
- if (d < best || d === best && (bestId === "" || pins[i].pinId < bestId)) {
129
- best = d;
130
- bestId = pins[i].pinId;
131
- u = i;
132
- }
133
- }
134
- }
135
- inTree[u] = true;
136
- if (parent[u] !== -1) {
137
- edges.push([pins[u].pinId, pins[parent[u]].pinId]);
138
- }
139
- for (let v = 0; v < n; v++) {
140
- if (!inTree[v]) {
141
- const d0 = manhattan(pins[u], pins[v]);
142
- const isForbidden = opts?.forbidEdge?.(pins[u], pins[v]) ?? false;
143
- const d = d0 > maxDistance || isForbidden ? Number.POSITIVE_INFINITY : d0;
144
- if (d < bestDist[v] || d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId) {
145
- bestDist[v] = d;
146
- parent[v] = u;
147
- }
148
- }
95
+ return sectionByPinId.get(pin.pinId);
96
+ };
97
+ var arePinsInDifferentSchematicSections = (inputProblem, p1, p2) => {
98
+ const sectionByChipId = /* @__PURE__ */ new Map();
99
+ const sectionByPinId = /* @__PURE__ */ new Map();
100
+ for (const chip of inputProblem.chips) {
101
+ if (!chip.sectionId) continue;
102
+ sectionByChipId.set(chip.chipId, chip.sectionId);
103
+ for (const pin of chip.pins) {
104
+ sectionByPinId.set(pin.pinId, chip.sectionId);
149
105
  }
150
106
  }
151
- return edges;
152
- }
107
+ if (sectionByChipId.size === 0) return false;
108
+ const s1 = getSectionNameForPin(sectionByChipId, sectionByPinId, p1);
109
+ const s2 = getSectionNameForPin(sectionByChipId, sectionByPinId, p2);
110
+ return s1 !== s2;
111
+ };
153
112
 
154
113
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection.ts
155
114
  var getPinDirection = (pin, chip) => {
@@ -181,6 +140,101 @@ var getPinDirection = (pin, chip) => {
181
140
  return "x-";
182
141
  };
183
142
 
143
+ // lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts
144
+ var visualizeInputProblem = (inputProblem, opts = {}) => {
145
+ const {
146
+ connectionAlpha = 0.8,
147
+ chipAlpha = 0.8,
148
+ hideRatsNet = inputProblem._hideRatsNet ?? false
149
+ } = opts;
150
+ const graphics = {
151
+ lines: [],
152
+ points: [],
153
+ rects: []
154
+ };
155
+ const pinIdMap = /* @__PURE__ */ new Map();
156
+ for (const chip of inputProblem.chips) {
157
+ for (const pin of chip.pins) {
158
+ pinIdMap.set(pin.pinId, pin);
159
+ }
160
+ }
161
+ for (const chip of inputProblem.chips) {
162
+ graphics.rects.push({
163
+ label: chip.chipId,
164
+ center: chip.center,
165
+ width: chip.width,
166
+ height: chip.height,
167
+ fill: getColorFromString(chip.chipId, chipAlpha)
168
+ });
169
+ for (const pin of chip.pins) {
170
+ graphics.points.push({
171
+ label: `${pin.pinId}
172
+ ${pin._facingDirection ?? getPinDirection(pin, chip)}`,
173
+ x: pin.x,
174
+ y: pin.y,
175
+ color: getColorFromString(pin.pinId, 0.8)
176
+ });
177
+ }
178
+ }
179
+ for (const textBox of inputProblem.textBoxes ?? []) {
180
+ graphics.rects.push({
181
+ label: textBox.text ?? "schematic_text",
182
+ center: textBox.center,
183
+ width: textBox.width,
184
+ height: textBox.height,
185
+ fill: "rgba(160, 0, 220, 0.14)",
186
+ strokeColor: "rgba(160, 0, 220, 0.9)"
187
+ });
188
+ }
189
+ if (!hideRatsNet) {
190
+ for (const directConn of inputProblem.directConnections) {
191
+ const [pinId1, pinId2] = directConn.pinIds;
192
+ const pin1 = pinIdMap.get(pinId1);
193
+ const pin2 = pinIdMap.get(pinId2);
194
+ if (arePinsInDifferentSchematicSections(inputProblem, pin1, pin2)) {
195
+ continue;
196
+ }
197
+ graphics.lines.push({
198
+ points: [
199
+ {
200
+ x: pin1.x,
201
+ y: pin1.y
202
+ },
203
+ {
204
+ x: pin2.x,
205
+ y: pin2.y
206
+ }
207
+ ],
208
+ strokeColor: getColorFromString(
209
+ directConn.netId ?? `${pinId1}-${pinId2}`,
210
+ connectionAlpha
211
+ )
212
+ });
213
+ }
214
+ for (const netConn of inputProblem.netConnections) {
215
+ const pins = netConn.pinIds.map((pinId) => pinIdMap.get(pinId));
216
+ for (let i = 0; i < pins.length - 1; i++) {
217
+ for (let j = i + 1; j < pins.length; j++) {
218
+ const pin1 = pins[i];
219
+ const pin2 = pins[j];
220
+ if (arePinsInDifferentSchematicSections(inputProblem, pin1, pin2)) {
221
+ continue;
222
+ }
223
+ graphics.lines.push({
224
+ points: [
225
+ { x: pin1.x, y: pin1.y },
226
+ { x: pin2.x, y: pin2.y }
227
+ ],
228
+ strokeColor: getColorFromString(netConn.netId, connectionAlpha),
229
+ strokeDash: "4 2"
230
+ });
231
+ }
232
+ }
233
+ }
234
+ }
235
+ return graphics;
236
+ };
237
+
184
238
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts
185
239
  var getRestrictedCenterLines = (params) => {
186
240
  const { pins, inputProblem, pinIdMap, chipMap } = params;
@@ -293,134 +347,81 @@ var doesPairCrossRestrictedCenterLines = (params) => {
293
347
  return hvCrosses && vhCrosses;
294
348
  };
295
349
 
296
- // lib/utils/getColorFromString.ts
297
- var getColorFromString = (string, alpha = 1) => {
298
- const hash = string.split("").reduce((acc, char) => {
299
- return acc * 31 + char.charCodeAt(0);
300
- }, 0);
301
- return `hsl(${hash % 360}, 100%, 50%, ${alpha})`;
302
- };
303
-
304
- // lib/utils/arePinsInDifferentSchematicSections.ts
305
- var getSectionNameForPin = (sectionByChipId, sectionByPinId, pin) => {
306
- if (pin.chipId) {
307
- const chipSection = sectionByChipId.get(pin.chipId);
308
- if (chipSection) return chipSection;
350
+ // lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts
351
+ import { ConnectivityMap } from "connectivity-map";
352
+ var getConnectivityMapsFromInputProblem = (inputProblem) => {
353
+ const directConnMap = new ConnectivityMap({});
354
+ for (const directConn of inputProblem.directConnections) {
355
+ directConnMap.addConnections([
356
+ directConn.netId ? [directConn.netId, ...directConn.pinIds] : directConn.pinIds
357
+ ]);
309
358
  }
310
- return sectionByPinId.get(pin.pinId);
311
- };
312
- var arePinsInDifferentSchematicSections = (inputProblem, p1, p2) => {
313
- const sectionByChipId = /* @__PURE__ */ new Map();
314
- const sectionByPinId = /* @__PURE__ */ new Map();
315
- for (const chip of inputProblem.chips) {
316
- if (!chip.sectionId) continue;
317
- sectionByChipId.set(chip.chipId, chip.sectionId);
318
- for (const pin of chip.pins) {
319
- sectionByPinId.set(pin.pinId, chip.sectionId);
320
- }
359
+ const netConnMap = new ConnectivityMap(directConnMap.netMap);
360
+ for (const netConn of inputProblem.netConnections) {
361
+ netConnMap.addConnections([[netConn.netId, ...netConn.pinIds]]);
321
362
  }
322
- if (sectionByChipId.size === 0) return false;
323
- const s1 = getSectionNameForPin(sectionByChipId, sectionByPinId, p1);
324
- const s2 = getSectionNameForPin(sectionByChipId, sectionByPinId, p2);
325
- return s1 !== s2;
363
+ return { directConnMap, netConnMap };
326
364
  };
327
365
 
328
- // lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts
329
- var visualizeInputProblem = (inputProblem, opts = {}) => {
330
- const {
331
- connectionAlpha = 0.8,
332
- chipAlpha = 0.8,
333
- hideRatsNet = inputProblem._hideRatsNet ?? false
334
- } = opts;
335
- const graphics = {
336
- lines: [],
337
- points: [],
338
- rects: []
339
- };
340
- const pinIdMap = /* @__PURE__ */ new Map();
341
- for (const chip of inputProblem.chips) {
342
- for (const pin of chip.pins) {
343
- pinIdMap.set(pin.pinId, pin);
344
- }
345
- }
346
- for (const chip of inputProblem.chips) {
347
- graphics.rects.push({
348
- label: chip.chipId,
349
- center: chip.center,
350
- width: chip.width,
351
- height: chip.height,
352
- fill: getColorFromString(chip.chipId, chipAlpha)
353
- });
354
- for (const pin of chip.pins) {
355
- graphics.points.push({
356
- label: `${pin.pinId}
357
- ${pin._facingDirection ?? getPinDirection(pin, chip)}`,
358
- x: pin.x,
359
- y: pin.y,
360
- color: getColorFromString(pin.pinId, 0.8)
361
- });
366
+ // lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts
367
+ function getOrthogonalMinimumSpanningTree(pins, opts = {}) {
368
+ const n = pins.length;
369
+ const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY;
370
+ if (n <= 1) return [];
371
+ {
372
+ const seen = /* @__PURE__ */ new Set();
373
+ for (const p of pins) {
374
+ if (seen.has(p.pinId)) {
375
+ throw new Error(`Duplicate pinId detected: "${p.pinId}"`);
376
+ }
377
+ seen.add(p.pinId);
362
378
  }
363
379
  }
364
- for (const textBox of inputProblem.textBoxes ?? []) {
365
- graphics.rects.push({
366
- label: textBox.text ?? "schematic_text",
367
- center: textBox.center,
368
- width: textBox.width,
369
- height: textBox.height,
370
- fill: "rgba(160, 0, 220, 0.14)",
371
- strokeColor: "rgba(160, 0, 220, 0.9)"
372
- });
380
+ const manhattan = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
381
+ const inTree = new Array(n).fill(false);
382
+ const bestDist = new Array(n).fill(Number.POSITIVE_INFINITY);
383
+ const parent = new Array(n).fill(-1);
384
+ let startIndex = 0;
385
+ for (let i = 1; i < n; i++) {
386
+ if (pins[i].pinId < pins[startIndex].pinId) startIndex = i;
373
387
  }
374
- if (!hideRatsNet) {
375
- for (const directConn of inputProblem.directConnections) {
376
- const [pinId1, pinId2] = directConn.pinIds;
377
- const pin1 = pinIdMap.get(pinId1);
378
- const pin2 = pinIdMap.get(pinId2);
379
- if (arePinsInDifferentSchematicSections(inputProblem, pin1, pin2)) {
380
- continue;
388
+ bestDist[startIndex] = 0;
389
+ const edges = [];
390
+ for (let iter = 0; iter < n; iter++) {
391
+ let u = -1;
392
+ let best = Number.POSITIVE_INFINITY;
393
+ let bestId = "";
394
+ for (let i = 0; i < n; i++) {
395
+ if (!inTree[i]) {
396
+ const d = bestDist[i];
397
+ if (d < best || d === best && (bestId === "" || pins[i].pinId < bestId)) {
398
+ best = d;
399
+ bestId = pins[i].pinId;
400
+ u = i;
401
+ }
381
402
  }
382
- graphics.lines.push({
383
- points: [
384
- {
385
- x: pin1.x,
386
- y: pin1.y
387
- },
388
- {
389
- x: pin2.x,
390
- y: pin2.y
391
- }
392
- ],
393
- strokeColor: getColorFromString(
394
- directConn.netId ?? `${pinId1}-${pinId2}`,
395
- connectionAlpha
396
- )
397
- });
398
403
  }
399
- for (const netConn of inputProblem.netConnections) {
400
- const pins = netConn.pinIds.map((pinId) => pinIdMap.get(pinId));
401
- for (let i = 0; i < pins.length - 1; i++) {
402
- for (let j = i + 1; j < pins.length; j++) {
403
- const pin1 = pins[i];
404
- const pin2 = pins[j];
405
- if (arePinsInDifferentSchematicSections(inputProblem, pin1, pin2)) {
406
- continue;
407
- }
408
- graphics.lines.push({
409
- points: [
410
- { x: pin1.x, y: pin1.y },
411
- { x: pin2.x, y: pin2.y }
412
- ],
413
- strokeColor: getColorFromString(netConn.netId, connectionAlpha),
414
- strokeDash: "4 2"
415
- });
404
+ inTree[u] = true;
405
+ if (parent[u] !== -1) {
406
+ edges.push([pins[u].pinId, pins[parent[u]].pinId]);
407
+ }
408
+ for (let v = 0; v < n; v++) {
409
+ if (!inTree[v]) {
410
+ const d0 = manhattan(pins[u], pins[v]);
411
+ const isForbidden = opts?.forbidEdge?.(pins[u], pins[v]) ?? false;
412
+ const d = d0 > maxDistance || isForbidden ? Number.POSITIVE_INFINITY : d0;
413
+ if (d < bestDist[v] || d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId) {
414
+ bestDist[v] = d;
415
+ parent[v] = u;
416
416
  }
417
417
  }
418
418
  }
419
419
  }
420
- return graphics;
421
- };
420
+ return edges;
421
+ }
422
422
 
423
423
  // lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts
424
+ var getPinPairKey = (pinIds) => [...pinIds].sort().join("::");
424
425
  var MspConnectionPairSolver = class extends BaseSolver {
425
426
  inputProblem;
426
427
  mspConnectionPairs = [];
@@ -431,6 +432,7 @@ var MspConnectionPairSolver = class extends BaseSolver {
431
432
  maxMspPairDistance;
432
433
  pinMap;
433
434
  userNetIdByPinId;
435
+ directConnectionPinPairKeys;
434
436
  constructor({ inputProblem }) {
435
437
  super();
436
438
  this.inputProblem = inputProblem;
@@ -449,7 +451,9 @@ var MspConnectionPairSolver = class extends BaseSolver {
449
451
  this.chipMap[chip.chipId] = chip;
450
452
  }
451
453
  this.userNetIdByPinId = {};
454
+ this.directConnectionPinPairKeys = /* @__PURE__ */ new Set();
452
455
  for (const dc of inputProblem.directConnections) {
456
+ this.directConnectionPinPairKeys.add(getPinPairKey(dc.pinIds));
453
457
  if (dc.netId) {
454
458
  const [a, b] = dc.pinIds;
455
459
  this.userNetIdByPinId[a] = dc.netId;
@@ -483,8 +487,9 @@ var MspConnectionPairSolver = class extends BaseSolver {
483
487
  const [pin1, pin2] = directlyConnectedPins;
484
488
  const p1 = this.pinMap[pin1];
485
489
  const p2 = this.pinMap[pin2];
486
- const manhattanDist = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
487
- if (manhattanDist > this.maxMspPairDistance) {
490
+ const pinPairKey = getPinPairKey([pin1, pin2]);
491
+ const pairDistance = this.directConnectionPinPairKeys.has(pinPairKey) ? distance(p1, p2) : Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
492
+ if (pairDistance > this.maxMspPairDistance) {
488
493
  return;
489
494
  }
490
495
  if (arePinsInDifferentSchematicSections(this.inputProblem, p1, p2)) {
@@ -796,6 +801,7 @@ var isPathCollidingWithObstacles = (path, obstacles) => {
796
801
 
797
802
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts
798
803
  var EPS2 = 1e-9;
804
+ var OBSTACLE_CLEARANCE = 0.2;
799
805
  var aabbFromPoints = (a, b) => ({
800
806
  minX: Math.min(a.x, b.x),
801
807
  maxX: Math.max(a.x, b.x),
@@ -810,7 +816,7 @@ var midBetweenPointAndRect = (axis, p, r, eps = EPS2) => {
810
816
  if (p.x > r.maxX + eps) {
811
817
  return [(p.x + r.maxX) / 2];
812
818
  }
813
- return [r.minX - 0.2, r.maxX + 0.2];
819
+ return [r.minX - OBSTACLE_CLEARANCE, r.maxX + OBSTACLE_CLEARANCE];
814
820
  } else {
815
821
  if (p.y < r.minY - eps) {
816
822
  return [(p.y + r.minY) / 2];
@@ -818,10 +824,11 @@ var midBetweenPointAndRect = (axis, p, r, eps = EPS2) => {
818
824
  if (p.y > r.maxY + eps) {
819
825
  return [(p.y + r.maxY) / 2];
820
826
  }
821
- return [r.minY - 0.2, r.maxY + 0.2];
827
+ return [r.minY - OBSTACLE_CLEARANCE, r.maxY + OBSTACLE_CLEARANCE];
822
828
  }
823
829
  };
824
- var candidateMidsFromSet = (axis, colliding, collisionRects, aabb, eps = EPS2) => {
830
+ var candidateMidsFromSet = (axis, colliding, collisionRects, aabb, opts = {}) => {
831
+ const { allowOpenSideCandidates = false, eps = EPS2 } = opts;
825
832
  const setRects = [...collisionRects];
826
833
  if (axis === "x") {
827
834
  const leftBoundaries = [aabb.minX, ...setRects.map((r) => r.maxX)].filter(
@@ -835,9 +842,13 @@ var candidateMidsFromSet = (axis, colliding, collisionRects, aabb, eps = EPS2) =
835
842
  const out = [];
836
843
  if (leftNeighbor !== void 0) {
837
844
  out.push((leftNeighbor + colliding.minX) / 2);
845
+ } else if (allowOpenSideCandidates) {
846
+ out.push(colliding.minX - OBSTACLE_CLEARANCE);
838
847
  }
839
848
  if (rightNeighbor !== void 0) {
840
849
  out.push((colliding.maxX + rightNeighbor) / 2);
850
+ } else if (allowOpenSideCandidates) {
851
+ out.push(colliding.maxX + OBSTACLE_CLEARANCE);
841
852
  }
842
853
  return out;
843
854
  } else {
@@ -852,14 +863,80 @@ var candidateMidsFromSet = (axis, colliding, collisionRects, aabb, eps = EPS2) =
852
863
  const out = [];
853
864
  if (bottomNeighbor !== void 0) {
854
865
  out.push((bottomNeighbor + colliding.minY) / 2);
866
+ } else if (allowOpenSideCandidates) {
867
+ out.push(colliding.minY - OBSTACLE_CLEARANCE);
855
868
  }
856
869
  if (topNeighbor !== void 0) {
857
870
  out.push((colliding.maxY + topNeighbor) / 2);
871
+ } else if (allowOpenSideCandidates) {
872
+ out.push(colliding.maxY + OBSTACLE_CLEARANCE);
858
873
  }
859
874
  return out;
860
875
  }
861
876
  };
862
877
 
878
+ // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts
879
+ var getSegmentAxis = (start, end) => {
880
+ if (isVertical(start, end)) return "x";
881
+ if (isHorizontal(start, end)) return "y";
882
+ return null;
883
+ };
884
+ var hasOnlyNonzeroOrthogonalSegments = (path) => path.every((point, index) => {
885
+ const nextPoint = path[index + 1];
886
+ if (!nextPoint) return true;
887
+ if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
888
+ return false;
889
+ }
890
+ return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0;
891
+ });
892
+ var generateEndpointCollisionDetours = ({
893
+ path,
894
+ collidingSegmentIndex,
895
+ obstacle
896
+ }) => {
897
+ if (path.length !== 3) return [];
898
+ const lastSegmentIndex = path.length - 2;
899
+ if (collidingSegmentIndex !== 0 && collidingSegmentIndex !== lastSegmentIndex) {
900
+ return [];
901
+ }
902
+ const shouldReverse = collidingSegmentIndex === lastSegmentIndex;
903
+ const orderedPath = shouldReverse ? [...path].reverse() : path;
904
+ const [start, corner, end] = orderedPath;
905
+ const firstSegmentAxis = getSegmentAxis(start, corner);
906
+ const secondSegmentAxis = getSegmentAxis(corner, end);
907
+ if (!firstSegmentAxis || !secondSegmentAxis) return [];
908
+ if (firstSegmentAxis === secondSegmentAxis) return [];
909
+ const escapeCoordinates = [
910
+ ...midBetweenPointAndRect(secondSegmentAxis, start, obstacle),
911
+ ...midBetweenPointAndRect(secondSegmentAxis, end, obstacle)
912
+ ];
913
+ const detourCoordinates = [
914
+ ...midBetweenPointAndRect(firstSegmentAxis, start, obstacle),
915
+ ...midBetweenPointAndRect(firstSegmentAxis, end, obstacle)
916
+ ];
917
+ const detours = [];
918
+ for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
919
+ for (const detourCoordinate of [...new Set(detourCoordinates)]) {
920
+ const orderedDetour = firstSegmentAxis === "y" ? [
921
+ start,
922
+ { x: escapeCoordinate, y: start.y },
923
+ { x: escapeCoordinate, y: detourCoordinate },
924
+ { x: end.x, y: detourCoordinate },
925
+ end
926
+ ] : [
927
+ start,
928
+ { x: start.x, y: escapeCoordinate },
929
+ { x: detourCoordinate, y: escapeCoordinate },
930
+ { x: detourCoordinate, y: end.y },
931
+ end
932
+ ];
933
+ const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour;
934
+ if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour);
935
+ }
936
+ }
937
+ return detours;
938
+ };
939
+
863
940
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts
864
941
  var EPS3 = 1e-9;
865
942
  var shiftSegmentOrth = (pts, segIndex, axis, newCoord, eps = EPS3) => {
@@ -1210,6 +1287,29 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1210
1287
  let { segIndex, rect } = collision;
1211
1288
  const isFirstSegment = segIndex === 0;
1212
1289
  const isLastSegment = segIndex === path.length - 2;
1290
+ if (path.length === 3 && (isFirstSegment || isLastSegment)) {
1291
+ const detours = generateEndpointCollisionDetours({
1292
+ path,
1293
+ collidingSegmentIndex: segIndex,
1294
+ obstacle: rect
1295
+ }).filter((detour) => {
1296
+ const key = pathKey(detour);
1297
+ if (this.visited.has(key)) return false;
1298
+ this.visited.add(key);
1299
+ return true;
1300
+ }).sort(
1301
+ (a2, b2) => this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2)
1302
+ );
1303
+ for (const detour of detours) {
1304
+ const nextCollisionRects = new Set(collisionRects);
1305
+ nextCollisionRects.add(rect);
1306
+ this.queue.push({
1307
+ path: detour,
1308
+ collisionRects: nextCollisionRects
1309
+ });
1310
+ }
1311
+ return;
1312
+ }
1213
1313
  if (isFirstSegment) {
1214
1314
  if (path.length < 3) {
1215
1315
  return;
@@ -1228,6 +1328,9 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1228
1328
  return;
1229
1329
  }
1230
1330
  const candidates = [];
1331
+ const candidateMidOptions = {
1332
+ allowOpenSideCandidates: this.connectionPair !== void 0
1333
+ };
1231
1334
  if (collisionRects.size === 0) {
1232
1335
  const m1 = midBetweenPointAndRect(axis, { x: PA.x, y: PA.y }, rect);
1233
1336
  const m2 = midBetweenPointAndRect(axis, { x: PB.x, y: PB.y }, rect);
@@ -1235,7 +1338,13 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1235
1338
  const uniqueCandidates = [...new Set(allCandidates)];
1236
1339
  candidates.push(...uniqueCandidates);
1237
1340
  } else {
1238
- const mids = candidateMidsFromSet(axis, rect, collisionRects, this.aabb);
1341
+ const mids = candidateMidsFromSet(
1342
+ axis,
1343
+ rect,
1344
+ collisionRects,
1345
+ this.aabb,
1346
+ candidateMidOptions
1347
+ );
1239
1348
  candidates.push(...mids);
1240
1349
  }
1241
1350
  const newStates = [];
@@ -1281,7 +1390,8 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1281
1390
  adjacentAxis,
1282
1391
  rect,
1283
1392
  collisionRects,
1284
- this.aabb
1393
+ this.aabb,
1394
+ candidateMidOptions
1285
1395
  )
1286
1396
  );
1287
1397
  }
@@ -2961,7 +3071,7 @@ var mergeLabelGroup = (group, groupKey) => {
2961
3071
  };
2962
3072
 
2963
3073
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts
2964
- import { distance } from "@tscircuit/math-utils";
3074
+ import { distance as distance2 } from "@tscircuit/math-utils";
2965
3075
  var filterLabelsAtTraceEdges = ({
2966
3076
  labels,
2967
3077
  traces,
@@ -2994,8 +3104,8 @@ var filterLabelsAtTraceEdges = ({
2994
3104
  if (trace.tracePath.length === 0) continue;
2995
3105
  const startPoint = trace.tracePath[0];
2996
3106
  const endPoint = trace.tracePath[trace.tracePath.length - 1];
2997
- const startDist = distance(label.center, startPoint);
2998
- const endDist = distance(label.center, endPoint);
3107
+ const startDist = distance2(label.center, startPoint);
3108
+ const endDist = distance2(label.center, endPoint);
2999
3109
  if (startDist <= distanceThreshold || endDist <= distanceThreshold) {
3000
3110
  isNearTraceEdge = true;
3001
3111
  break;
@@ -4077,7 +4187,7 @@ function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
4077
4187
 
4078
4188
  // lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts
4079
4189
  var NEAREST_NEIGHBOR_COUNT = 3;
4080
- var distance2 = (p1, p2) => {
4190
+ var distance3 = (p1, p2) => {
4081
4191
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
4082
4192
  };
4083
4193
  var LongDistancePairSolver = class extends BaseSolver {
@@ -4118,7 +4228,7 @@ var LongDistancePairSolver = class extends BaseSolver {
4118
4228
  return [
4119
4229
  {
4120
4230
  pin: targetPin,
4121
- distance: distance2(sourcePin, targetPin)
4231
+ distance: distance3(sourcePin, targetPin)
4122
4232
  }
4123
4233
  ];
4124
4234
  }).sort((a, b) => a.distance - b.distance).slice(0, NEAREST_NEIGHBOR_COUNT);
@@ -4932,10 +5042,10 @@ var projectPointToPath = (point, path) => {
4932
5042
  let bestDistance = Number.POSITIVE_INFINITY;
4933
5043
  for (let i = 0; i < path.length - 1; i++) {
4934
5044
  const projectedPoint = projectPointToSegment(point, path[i], path[i + 1]);
4935
- const distance3 = getDistance2(point, projectedPoint);
4936
- if (distance3 < bestDistance) {
5045
+ const distance4 = getDistance2(point, projectedPoint);
5046
+ if (distance4 < bestDistance) {
4937
5047
  bestPoint = projectedPoint;
4938
- bestDistance = distance3;
5048
+ bestDistance = distance4;
4939
5049
  }
4940
5050
  }
4941
5051
  return bestPoint;
@@ -6565,11 +6675,11 @@ var getLabelHugDistance = (tracePath, obstacleLabel) => {
6565
6675
  obstacleLabel.width,
6566
6676
  obstacleLabel.height
6567
6677
  );
6568
- let distance3 = 0;
6678
+ let distance4 = 0;
6569
6679
  for (const point of tracePath) {
6570
- distance3 += getPointDistanceFromRect(point, bounds);
6680
+ distance4 += getPointDistanceFromRect(point, bounds);
6571
6681
  }
6572
- return distance3;
6682
+ return distance4;
6573
6683
  };
6574
6684
  var getPointDistanceFromRect = (point, rect) => {
6575
6685
  const dx = Math.max(rect.minX - point.x, 0, point.x - rect.maxX);
@@ -6846,16 +6956,16 @@ var Example28Solver = class extends BaseSolver {
6846
6956
  const outward = dir(label.orientation);
6847
6957
  if (outward.x === 0 && outward.y === 0) return null;
6848
6958
  for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
6849
- const distance3 = step * LABEL_OUTWARD_STEP;
6959
+ const distance4 = step * LABEL_OUTWARD_STEP;
6850
6960
  const candidate = {
6851
6961
  ...label,
6852
6962
  anchorPoint: {
6853
- x: label.anchorPoint.x + outward.x * distance3,
6854
- y: label.anchorPoint.y + outward.y * distance3
6963
+ x: label.anchorPoint.x + outward.x * distance4,
6964
+ y: label.anchorPoint.y + outward.y * distance4
6855
6965
  },
6856
6966
  center: {
6857
- x: label.center.x + outward.x * distance3,
6858
- y: label.center.y + outward.y * distance3
6967
+ x: label.center.x + outward.x * distance4,
6968
+ y: label.center.y + outward.y * distance4
6859
6969
  }
6860
6970
  };
6861
6971
  const candidateWithClearance = {
@@ -7393,10 +7503,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7393
7503
  outwardDistance,
7394
7504
  phase = "shift"
7395
7505
  } = params;
7396
- for (let distance3 = LABEL_SEARCH_STEP; distance3 <= maxSearchDistance + EPS9; distance3 += LABEL_SEARCH_STEP) {
7506
+ for (let distance4 = LABEL_SEARCH_STEP; distance4 <= maxSearchDistance + EPS9; distance4 += LABEL_SEARCH_STEP) {
7397
7507
  const anchorPoint = {
7398
- x: baseAnchor.x + direction.x * distance3,
7399
- y: baseAnchor.y + direction.y * distance3
7508
+ x: baseAnchor.x + direction.x * distance4,
7509
+ y: baseAnchor.y + direction.y * distance4
7400
7510
  };
7401
7511
  const candidate = this.createCandidate(label, anchorPoint, orientation);
7402
7512
  const result = this.evaluateCandidate(
@@ -7404,7 +7514,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7404
7514
  label,
7405
7515
  labelIndex,
7406
7516
  phase,
7407
- distance3,
7517
+ distance4,
7408
7518
  outwardDistance
7409
7519
  );
7410
7520
  this.currentCandidateResults.push(result);
@@ -7416,11 +7526,11 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7416
7526
  }
7417
7527
  return null;
7418
7528
  }
7419
- evaluateCandidate(candidate, label, labelIndex, phase, distance3, outwardDistance) {
7529
+ evaluateCandidate(candidate, label, labelIndex, phase, distance4, outwardDistance) {
7420
7530
  return {
7421
7531
  ...candidate,
7422
7532
  phase,
7423
- distance: distance3,
7533
+ distance: distance4,
7424
7534
  outwardDistance,
7425
7535
  selected: false,
7426
7536
  status: this.getCandidateStatus({
@@ -7758,10 +7868,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7758
7868
  if (point.x < bounds.minX - EPS9 || point.x > bounds.maxX + EPS9 || point.y < bounds.minY - EPS9 || point.y > bounds.maxY + EPS9) {
7759
7869
  continue;
7760
7870
  }
7761
- for (const [side, distance3] of getSideDistances(point, bounds)) {
7762
- if (distance3 < nearestDistance) {
7871
+ for (const [side, distance4] of getSideDistances(point, bounds)) {
7872
+ if (distance4 < nearestDistance) {
7763
7873
  nearestSide = side;
7764
- nearestDistance = distance3;
7874
+ nearestDistance = distance4;
7765
7875
  }
7766
7876
  }
7767
7877
  }
@@ -7790,10 +7900,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
7790
7900
  let nearestSide = null;
7791
7901
  let nearestDistance = Number.POSITIVE_INFINITY;
7792
7902
  for (const chip of this.chipObstacleSpatialIndex.chips) {
7793
- for (const [side, distance3] of getSideDistances(point, chip.bounds)) {
7794
- if (distance3 < nearestDistance) {
7903
+ for (const [side, distance4] of getSideDistances(point, chip.bounds)) {
7904
+ if (distance4 < nearestDistance) {
7795
7905
  nearestSide = side;
7796
- nearestDistance = distance3;
7906
+ nearestDistance = distance4;
7797
7907
  }
7798
7908
  }
7799
7909
  }
@@ -8194,17 +8304,17 @@ var getTraceLength = (trace) => {
8194
8304
  }
8195
8305
  return length;
8196
8306
  };
8197
- var getPointAtTraceDistance = (trace, distance3) => {
8307
+ var getPointAtTraceDistance = (trace, distance4) => {
8198
8308
  let pathDistance = 0;
8199
8309
  for (let i = 0; i < trace.tracePath.length - 1; i++) {
8200
8310
  const start = trace.tracePath[i];
8201
8311
  const end = trace.tracePath[i + 1];
8202
8312
  const segmentLength = getManhattanDistance(start, end);
8203
8313
  const nextDistance = pathDistance + segmentLength;
8204
- if (distance3 <= nextDistance + EPS10) {
8314
+ if (distance4 <= nextDistance + EPS10) {
8205
8315
  const offset = Math.max(
8206
8316
  0,
8207
- Math.min(segmentLength, distance3 - pathDistance)
8317
+ Math.min(segmentLength, distance4 - pathDistance)
8208
8318
  );
8209
8319
  const direction = getSegmentDirection(start, end);
8210
8320
  return {
@@ -8329,13 +8439,13 @@ var getCandidateDistances = (traceLength, vertexDistances) => {
8329
8439
  const distances = /* @__PURE__ */ new Set();
8330
8440
  const maxSteps = Math.ceil(traceLength / CANDIDATE_STEP);
8331
8441
  for (let i = 0; i <= maxSteps; i++) {
8332
- const distance3 = Math.min(traceLength, i * CANDIDATE_STEP);
8333
- distances.add(roundDistance(distance3));
8442
+ const distance4 = Math.min(traceLength, i * CANDIDATE_STEP);
8443
+ distances.add(roundDistance(distance4));
8334
8444
  }
8335
- for (const distance3 of vertexDistances) {
8336
- distances.add(roundDistance(distance3));
8445
+ for (const distance4 of vertexDistances) {
8446
+ distances.add(roundDistance(distance4));
8337
8447
  }
8338
- return [...distances].filter((distance3) => distance3 >= -EPS10 && distance3 <= traceLength + EPS10).sort((a, b) => a - b);
8448
+ return [...distances].filter((distance4) => distance4 >= -EPS10 && distance4 <= traceLength + EPS10).sort((a, b) => a - b);
8339
8449
  };
8340
8450
  var getOrientationsForPoint = (params) => {
8341
8451
  const { inputProblem, label, point, orientationConstraint } = params;
@@ -8513,7 +8623,7 @@ var getNetLabelHeight = (inputProblem, label) => {
8513
8623
  (nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid))
8514
8624
  )?.netLabelHeight;
8515
8625
  };
8516
- var roundDistance = (distance3) => Number(distance3.toFixed(6));
8626
+ var roundDistance = (distance4) => Number(distance4.toFixed(6));
8517
8627
  var isSamePlacement = (label, point, orientation) => Math.abs(point.x - label.anchorPoint.x) <= EPS10 && Math.abs(point.y - label.anchorPoint.y) <= EPS10 && orientation === label.orientation;
8518
8628
 
8519
8629
  // lib/solvers/TraceAnchoredNetLabelOverlapSolver/visualize.ts