@tscircuit/schematic-trace-solver 0.0.67 → 0.0.69

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/dist/index.js CHANGED
@@ -2799,19 +2799,32 @@ var detectTraceLabelOverlap = ({
2799
2799
  };
2800
2800
 
2801
2801
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts
2802
- var findTraceViolationZone = (path, labelBounds) => {
2803
- const isPointInside = (p) => p.x > labelBounds.minX && p.x < labelBounds.maxX && p.y > labelBounds.minY && p.y < labelBounds.maxY;
2804
- let firstInsideIndex = -1;
2805
- let lastInsideIndex = -1;
2806
- for (let i = 0; i < path.length; i++) {
2807
- if (isPointInside(path[i])) {
2808
- if (firstInsideIndex === -1) {
2809
- firstInsideIndex = i;
2810
- }
2811
- lastInsideIndex = i;
2802
+ var findTraceViolationZone = ({
2803
+ path,
2804
+ labelBounds,
2805
+ paddedBounds = labelBounds
2806
+ }) => {
2807
+ let firstCollidingSeg = -1;
2808
+ let lastCollidingSeg = -1;
2809
+ for (let i = 0; i < path.length - 1; i++) {
2810
+ if (segmentIntersectsRect2(path[i], path[i + 1], labelBounds)) {
2811
+ if (firstCollidingSeg === -1) firstCollidingSeg = i;
2812
+ lastCollidingSeg = i;
2812
2813
  }
2813
2814
  }
2814
- return { firstInsideIndex, lastInsideIndex };
2815
+ if (firstCollidingSeg === -1) {
2816
+ return { firstInsideIndex: -1, lastInsideIndex: -1 };
2817
+ }
2818
+ const isInsidePadded = (p) => p.x > paddedBounds.minX && p.x < paddedBounds.maxX && p.y > paddedBounds.minY && p.y < paddedBounds.maxY;
2819
+ let entryIndex = firstCollidingSeg;
2820
+ while (entryIndex > 0 && isInsidePadded(path[entryIndex])) {
2821
+ entryIndex--;
2822
+ }
2823
+ let exitIndex = lastCollidingSeg + 1;
2824
+ while (exitIndex < path.length - 1 && isInsidePadded(path[exitIndex])) {
2825
+ exitIndex++;
2826
+ }
2827
+ return { firstInsideIndex: entryIndex + 1, lastInsideIndex: exitIndex - 1 };
2815
2828
  };
2816
2829
 
2817
2830
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts
@@ -2917,20 +2930,21 @@ var generateFourPointDetourCandidates = ({
2917
2930
  }
2918
2931
  exitPoint = initialTrace.tracePath[exitIndex];
2919
2932
  } else {
2920
- let collidingSegIndex = -1;
2933
+ let firstCollidingSeg = -1;
2934
+ let lastCollidingSeg = -1;
2921
2935
  for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
2922
2936
  if (segmentIntersectsRect(
2923
2937
  initialTrace.tracePath[i],
2924
2938
  initialTrace.tracePath[i + 1],
2925
- { ...paddedLabelBounds, chipId: "temp-obstacle" }
2939
+ { ...labelBounds, chipId: "temp-obstacle" }
2926
2940
  )) {
2927
- collidingSegIndex = i;
2928
- break;
2941
+ if (firstCollidingSeg === -1) firstCollidingSeg = i;
2942
+ lastCollidingSeg = i;
2929
2943
  }
2930
2944
  }
2931
- if (collidingSegIndex === -1) return [];
2932
- entryIndex = collidingSegIndex;
2933
- exitIndex = collidingSegIndex + 1;
2945
+ if (firstCollidingSeg === -1) return [];
2946
+ entryIndex = firstCollidingSeg;
2947
+ exitIndex = lastCollidingSeg + 1;
2934
2948
  entryPoint = initialTrace.tracePath[entryIndex];
2935
2949
  exitPoint = initialTrace.tracePath[exitIndex];
2936
2950
  }
@@ -3039,10 +3053,18 @@ var generateRerouteCandidates = ({
3039
3053
  paddingBuffer,
3040
3054
  detourCount
3041
3055
  });
3042
- const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone(
3043
- initialTrace.tracePath,
3044
- labelBounds
3045
- );
3056
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer;
3057
+ const paddedLabelBounds = {
3058
+ minX: labelBounds.minX - effectivePadding,
3059
+ maxX: labelBounds.maxX + effectivePadding,
3060
+ minY: labelBounds.minY - effectivePadding,
3061
+ maxY: labelBounds.maxY + effectivePadding
3062
+ };
3063
+ const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone({
3064
+ path: initialTrace.tracePath,
3065
+ labelBounds,
3066
+ paddedBounds: paddedLabelBounds
3067
+ });
3046
3068
  const snipReconnectCandidates = generateSnipAndReconnectCandidates({
3047
3069
  initialTrace,
3048
3070
  firstInsideIndex,
@@ -3131,7 +3153,11 @@ var SingleOverlapSolver = class extends BaseSolver {
3131
3153
  this._tried++;
3132
3154
  const nextCandidatePath = this.queuedCandidatePaths.shift();
3133
3155
  const simplifiedPath = simplifyPath(nextCandidatePath);
3134
- if (!isPathCollidingWithObstacles(simplifiedPath, this.obstacles) && !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)) {
3156
+ const stillOverlapsLabel = detectTraceLabelOverlap({
3157
+ traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
3158
+ netLabels: [this.label]
3159
+ }).length > 0;
3160
+ if (!stillOverlapsLabel && !isPathCollidingWithObstacles(simplifiedPath, this.obstacles) && !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)) {
3135
3161
  this.solvedTracePath = simplifiedPath;
3136
3162
  this.solved = true;
3137
3163
  }
@@ -4550,6 +4576,7 @@ var visualizeCollision = (collisionInfo) => {
4550
4576
  // lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts
4551
4577
  var UntangleTraceSubsolver = class extends BaseSolver {
4552
4578
  input;
4579
+ chipObstacleSpatialIndex;
4553
4580
  lShapesToProcess = [];
4554
4581
  visualizationMode = "l_shapes";
4555
4582
  currentLShape = null;
@@ -4570,6 +4597,10 @@ var UntangleTraceSubsolver = class extends BaseSolver {
4570
4597
  super();
4571
4598
  this.input = solverInput;
4572
4599
  this.visualizationMode = "l_shapes";
4600
+ this.chipObstacleSpatialIndex = this.input.inputProblem._chipObstacleSpatialIndex ?? new ChipObstacleSpatialIndex(this.input.inputProblem.chips);
4601
+ if (!this.input.inputProblem._chipObstacleSpatialIndex) {
4602
+ this.input.inputProblem._chipObstacleSpatialIndex = this.chipObstacleSpatialIndex;
4603
+ }
4573
4604
  for (const trace of this.input.allTraces) {
4574
4605
  const lShapes = findAllLShapedTurns(trace.tracePath);
4575
4606
  this.lShapesToProcess.push(
@@ -4696,6 +4727,12 @@ var UntangleTraceSubsolver = class extends BaseSolver {
4696
4727
  this.input.allTraces,
4697
4728
  this.currentLShape.traceId
4698
4729
  );
4730
+ if (!collisionResult?.isColliding && this._doesCandidateCrossChip(currentCandidate)) {
4731
+ this.lastCollision = null;
4732
+ this.collidingCandidate = currentCandidate;
4733
+ this.currentCandidateIndex++;
4734
+ return;
4735
+ }
4699
4736
  if (!collisionResult?.isColliding) {
4700
4737
  this.bestRouteFound = currentCandidate;
4701
4738
  this.lastCollision = null;
@@ -4706,6 +4743,21 @@ var UntangleTraceSubsolver = class extends BaseSolver {
4706
4743
  this.currentCandidateIndex++;
4707
4744
  }
4708
4745
  }
4746
+ /**
4747
+ * Returns true if any segment of the candidate reroute passes through a
4748
+ * schematic component (chip) body.
4749
+ */
4750
+ _doesCandidateCrossChip(candidate) {
4751
+ for (let i = 0; i < candidate.length - 1; i++) {
4752
+ if (this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip([
4753
+ candidate[i],
4754
+ candidate[i + 1]
4755
+ ])) {
4756
+ return true;
4757
+ }
4758
+ }
4759
+ return false;
4760
+ }
4709
4761
  _applyBestRoute(bestRoute) {
4710
4762
  this.bestRoute = bestRoute;
4711
4763
  this.collidingCandidate = null;
@@ -2,6 +2,7 @@ import { BaseSolver } from "../../BaseSolver/BaseSolver"
2
2
  import type { InputProblem } from "../../../types/InputProblem"
3
3
  import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
4
  import type { NetLabelPlacement } from "../../NetLabelPlacementSolver/NetLabelPlacementSolver"
5
+ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
5
6
 
6
7
  import { findAllLShapedTurns, type LShape } from "./findAllLShapedTurns"
7
8
  import { getTraceObstacles } from "./getTraceObstacles"
@@ -57,6 +58,7 @@ type VisualizationMode =
57
58
  */
58
59
  export class UntangleTraceSubsolver extends BaseSolver {
59
60
  private input: UntangleTraceSubsolverInput
61
+ private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
60
62
  private lShapesToProcess: LShape[] = []
61
63
  private visualizationMode: VisualizationMode = "l_shapes"
62
64
 
@@ -86,6 +88,14 @@ export class UntangleTraceSubsolver extends BaseSolver {
86
88
  this.input = solverInput
87
89
  this.visualizationMode = "l_shapes"
88
90
 
91
+ this.chipObstacleSpatialIndex =
92
+ this.input.inputProblem._chipObstacleSpatialIndex ??
93
+ new ChipObstacleSpatialIndex(this.input.inputProblem.chips)
94
+ if (!this.input.inputProblem._chipObstacleSpatialIndex) {
95
+ this.input.inputProblem._chipObstacleSpatialIndex =
96
+ this.chipObstacleSpatialIndex
97
+ }
98
+
89
99
  for (const trace of this.input.allTraces) {
90
100
  const lShapes = findAllLShapedTurns(trace.tracePath)
91
101
  this.lShapesToProcess.push(
@@ -232,6 +242,20 @@ export class UntangleTraceSubsolver extends BaseSolver {
232
242
  this.currentLShape!.traceId,
233
243
  )
234
244
 
245
+ // Untangling must never move a trace through a component body. The candidate
246
+ // only covers the rerouted corner (not the pin-terminal segments), so reject
247
+ // any candidate that crosses a chip; the original (component-clear) path is
248
+ // kept instead, so this stage only ever improves or leaves the trace valid.
249
+ if (
250
+ !collisionResult?.isColliding &&
251
+ this._doesCandidateCrossChip(currentCandidate)
252
+ ) {
253
+ this.lastCollision = null
254
+ this.collidingCandidate = currentCandidate
255
+ this.currentCandidateIndex++
256
+ return
257
+ }
258
+
235
259
  if (!collisionResult?.isColliding) {
236
260
  this.bestRouteFound = currentCandidate
237
261
  this.lastCollision = null
@@ -243,6 +267,24 @@ export class UntangleTraceSubsolver extends BaseSolver {
243
267
  }
244
268
  }
245
269
 
270
+ /**
271
+ * Returns true if any segment of the candidate reroute passes through a
272
+ * schematic component (chip) body.
273
+ */
274
+ private _doesCandidateCrossChip(candidate: Point[]): boolean {
275
+ for (let i = 0; i < candidate.length - 1; i++) {
276
+ if (
277
+ this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip([
278
+ candidate[i]!,
279
+ candidate[i + 1]!,
280
+ ])
281
+ ) {
282
+ return true
283
+ }
284
+ }
285
+ return false
286
+ }
287
+
246
288
  private _applyBestRoute(bestRoute: Point[]) {
247
289
  this.bestRoute = bestRoute
248
290
  this.collidingCandidate = null
@@ -55,10 +55,19 @@ export const generateRerouteCandidates = ({
55
55
  detourCount,
56
56
  })
57
57
 
58
- const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone(
59
- initialTrace.tracePath,
58
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer
59
+ const paddedLabelBounds = {
60
+ minX: labelBounds.minX - effectivePadding,
61
+ maxX: labelBounds.maxX + effectivePadding,
62
+ minY: labelBounds.minY - effectivePadding,
63
+ maxY: labelBounds.maxY + effectivePadding,
64
+ }
65
+
66
+ const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone({
67
+ path: initialTrace.tracePath,
60
68
  labelBounds,
61
- )
69
+ paddedBounds: paddedLabelBounds,
70
+ })
62
71
 
63
72
  const snipReconnectCandidates = generateSnipAndReconnectCandidates({
64
73
  initialTrace,
@@ -9,6 +9,7 @@ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/Schemati
9
9
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
10
10
  import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
11
11
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
12
+ import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
12
13
 
13
14
  interface SingleOverlapSolverInput {
14
15
  trace: SolvedTracePath
@@ -141,7 +142,17 @@ export class SingleOverlapSolver extends BaseSolver {
141
142
  const nextCandidatePath = this.queuedCandidatePaths.shift()!
142
143
  const simplifiedPath = simplifyPath(nextCandidatePath)
143
144
 
145
+ // A candidate is only valid if it actually clears the label it is meant to
146
+ // avoid. Without this check a "detour" that still grazes the label (e.g. one
147
+ // built around the wrong segment) would be accepted as solved.
148
+ const stillOverlapsLabel =
149
+ detectTraceLabelOverlap({
150
+ traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
151
+ netLabels: [this.label],
152
+ }).length > 0
153
+
144
154
  if (
155
+ !stillOverlapsLabel &&
145
156
  !isPathCollidingWithObstacles(simplifiedPath, this.obstacles) &&
146
157
  !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)
147
158
  ) {
@@ -71,25 +71,30 @@ export const generateFourPointDetourCandidates = ({
71
71
  }
72
72
  exitPoint = initialTrace.tracePath[exitIndex]
73
73
  } else {
74
- // STRATEGY 1: Single Label - find the first intersecting segment
75
- let collidingSegIndex = -1
74
+ // STRATEGY 1: Single Label - reroute around the contiguous span of segments
75
+ // that actually overlap the label. Detection uses the *unpadded* bounds so
76
+ // a short pin stub that merely pokes into the padding zone is not mistaken
77
+ // for the real crossing (which would leave the offending segment in place
78
+ // and create a useless loop).
79
+ let firstCollidingSeg = -1
80
+ let lastCollidingSeg = -1
76
81
  for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
77
82
  if (
78
83
  segmentIntersectsRect(
79
84
  initialTrace.tracePath[i],
80
85
  initialTrace.tracePath[i + 1],
81
- { ...paddedLabelBounds, chipId: "temp-obstacle" },
86
+ { ...labelBounds, chipId: "temp-obstacle" },
82
87
  )
83
88
  ) {
84
- collidingSegIndex = i
85
- break
89
+ if (firstCollidingSeg === -1) firstCollidingSeg = i
90
+ lastCollidingSeg = i
86
91
  }
87
92
  }
88
93
 
89
- if (collidingSegIndex === -1) return []
94
+ if (firstCollidingSeg === -1) return []
90
95
 
91
- entryIndex = collidingSegIndex
92
- exitIndex = collidingSegIndex + 1
96
+ entryIndex = firstCollidingSeg
97
+ exitIndex = lastCollidingSeg + 1
93
98
  entryPoint = initialTrace.tracePath[entryIndex]
94
99
  exitPoint = initialTrace.tracePath[exitIndex]
95
100
  }
@@ -1,25 +1,59 @@
1
- import type { Point } from "@tscircuit/math-utils"
1
+ import type { Point, Bounds } from "@tscircuit/math-utils"
2
+ import { segmentIntersectsRect } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
2
3
 
3
- export const findTraceViolationZone = (
4
- path: Point[],
5
- labelBounds: { minX: number; maxX: number; minY: number; maxY: number },
6
- ) => {
7
- const isPointInside = (p: Point) =>
8
- p.x > labelBounds.minX &&
9
- p.x < labelBounds.maxX &&
10
- p.y > labelBounds.minY &&
11
- p.y < labelBounds.maxY
4
+ /**
5
+ * Locates the portion of a trace that must be rerouted to clear a label.
6
+ *
7
+ * Detection is segment-based against the (unpadded) label bounds so it also
8
+ * catches traces that cross the label edge-to-edge, or run *along* one of its
9
+ * edges, without ever placing a vertex strictly inside it (the previous
10
+ * vertex-only test missed both cases).
11
+ *
12
+ * The returned range describes the vertices to remove: the reroute reconnects
13
+ * `path[firstInsideIndex - 1]` to `path[lastInsideIndex + 1]`. Those two anchor
14
+ * vertices are pushed outward until they sit outside the padded label so the
15
+ * detour has room to pivot without immediately re-entering it.
16
+ */
17
+ export const findTraceViolationZone = ({
18
+ path,
19
+ labelBounds,
20
+ paddedBounds = labelBounds,
21
+ }: {
22
+ path: Point[]
23
+ labelBounds: Bounds
24
+ paddedBounds?: Bounds
25
+ }) => {
26
+ let firstCollidingSeg = -1
27
+ let lastCollidingSeg = -1
28
+ for (let i = 0; i < path.length - 1; i++) {
29
+ if (segmentIntersectsRect(path[i]!, path[i + 1]!, labelBounds)) {
30
+ if (firstCollidingSeg === -1) firstCollidingSeg = i
31
+ lastCollidingSeg = i
32
+ }
33
+ }
12
34
 
13
- let firstInsideIndex = -1
14
- let lastInsideIndex = -1
35
+ if (firstCollidingSeg === -1) {
36
+ return { firstInsideIndex: -1, lastInsideIndex: -1 }
37
+ }
15
38
 
16
- for (let i = 0; i < path.length; i++) {
17
- if (isPointInside(path[i])) {
18
- if (firstInsideIndex === -1) {
19
- firstInsideIndex = i
20
- }
21
- lastInsideIndex = i
22
- }
39
+ const isInsidePadded = (p: Point) =>
40
+ p.x > paddedBounds.minX &&
41
+ p.x < paddedBounds.maxX &&
42
+ p.y > paddedBounds.minY &&
43
+ p.y < paddedBounds.maxY
44
+
45
+ // Anchor before the crossing: walk back to the first vertex clear of the
46
+ // padded label.
47
+ let entryIndex = firstCollidingSeg
48
+ while (entryIndex > 0 && isInsidePadded(path[entryIndex]!)) {
49
+ entryIndex--
50
+ }
51
+
52
+ // Anchor after the crossing.
53
+ let exitIndex = lastCollidingSeg + 1
54
+ while (exitIndex < path.length - 1 && isInsidePadded(path[exitIndex]!)) {
55
+ exitIndex++
23
56
  }
24
- return { firstInsideIndex, lastInsideIndex }
57
+
58
+ return { firstInsideIndex: entryIndex + 1, lastInsideIndex: exitIndex - 1 }
25
59
  }
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.67",
4
+ "version": "0.0.69",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",