@tscircuit/schematic-trace-solver 0.0.68 → 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 +49 -23
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +12 -3
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +11 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +13 -8
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts +54 -20
- package/package.json +1 -1
- package/tests/examples/__snapshots__/example03.snap.svg +66 -66
- package/tests/examples/__snapshots__/example16.snap.svg +5 -9
- package/tests/examples/__snapshots__/example20.snap.svg +4 -7
- package/tests/examples/__snapshots__/example22.snap.svg +5 -9
- package/tests/examples/__snapshots__/example32.snap.svg +108 -93
- package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +396 -0
- package/tests/repros/assets/repro-bq24074-battery-charger.input.json +219 -0
- package/tests/repros/repro-bq24074-battery-charger.test.ts +18 -0
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 = (
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
{ ...
|
|
2939
|
+
{ ...labelBounds, chipId: "temp-obstacle" }
|
|
2926
2940
|
)) {
|
|
2927
|
-
|
|
2928
|
-
|
|
2941
|
+
if (firstCollidingSeg === -1) firstCollidingSeg = i;
|
|
2942
|
+
lastCollidingSeg = i;
|
|
2929
2943
|
}
|
|
2930
2944
|
}
|
|
2931
|
-
if (
|
|
2932
|
-
entryIndex =
|
|
2933
|
-
exitIndex =
|
|
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
|
|
3043
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -55,10 +55,19 @@ export const generateRerouteCandidates = ({
|
|
|
55
55
|
detourCount,
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
const
|
|
59
|
-
|
|
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 -
|
|
75
|
-
|
|
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
|
-
{ ...
|
|
86
|
+
{ ...labelBounds, chipId: "temp-obstacle" },
|
|
82
87
|
)
|
|
83
88
|
) {
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
if (firstCollidingSeg === -1) firstCollidingSeg = i
|
|
90
|
+
lastCollidingSeg = i
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
if (
|
|
94
|
+
if (firstCollidingSeg === -1) return []
|
|
90
95
|
|
|
91
|
-
entryIndex =
|
|
92
|
-
exitIndex =
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
35
|
+
if (firstCollidingSeg === -1) {
|
|
36
|
+
return { firstInsideIndex: -1, lastInsideIndex: -1 }
|
|
37
|
+
}
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
57
|
+
|
|
58
|
+
return { firstInsideIndex: entryIndex + 1, lastInsideIndex: exitIndex - 1 }
|
|
25
59
|
}
|