@tscircuit/schematic-trace-solver 0.0.120 → 0.0.121

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
@@ -3922,7 +3922,12 @@ var generateRerouteCandidates = ({
3922
3922
  };
3923
3923
 
3924
3924
  // lib/utils/doesPathCoincideWithTraces.ts
3925
+ import { boundsIntersection } from "@tscircuit/math-utils";
3925
3926
  var COINCIDENT_EPS = 2e-3;
3927
+ var GEOMETRY_EPS = 1e-6;
3928
+ var SCHEMATIC_TRACE_STROKE_WIDTH = 0.02;
3929
+ var SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE = SCHEMATIC_TRACE_STROKE_WIDTH + GEOMETRY_EPS;
3930
+ var SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE = SCHEMATIC_TRACE_STROKE_WIDTH * 3;
3926
3931
  var doesPathCoincideWithTraces = (path, traces) => {
3927
3932
  const rangesOverlap1D = (a1, a2, b1, b2) => Math.min(Math.max(a1, a2), Math.max(b1, b2)) - Math.max(Math.min(a1, a2), Math.min(b1, b2)) > COINCIDENT_EPS;
3928
3933
  for (let i = 0; i < path.length - 1; i++) {
@@ -3954,6 +3959,44 @@ var doesPathCoincideWithTraces = (path, traces) => {
3954
3959
  }
3955
3960
  return false;
3956
3961
  };
3962
+ var doesPathOverlapTraceStrokes = (path, traces) => {
3963
+ const traceStrokeRadius = SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE / 2;
3964
+ const getStrokeBounds = (start, end, isVertical5) => isVertical5 ? {
3965
+ minX: start.x - traceStrokeRadius,
3966
+ maxX: start.x + traceStrokeRadius,
3967
+ minY: Math.min(start.y, end.y),
3968
+ maxY: Math.max(start.y, end.y)
3969
+ } : {
3970
+ minX: Math.min(start.x, end.x),
3971
+ maxX: Math.max(start.x, end.x),
3972
+ minY: start.y - traceStrokeRadius,
3973
+ maxY: start.y + traceStrokeRadius
3974
+ };
3975
+ for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
3976
+ const pathStart = path[pathIndex];
3977
+ const pathEnd = path[pathIndex + 1];
3978
+ const isVertical5 = Math.abs(pathStart.x - pathEnd.x) < COINCIDENT_EPS;
3979
+ const isHorizontal4 = Math.abs(pathStart.y - pathEnd.y) < COINCIDENT_EPS;
3980
+ if (!isVertical5 && !isHorizontal4) continue;
3981
+ const pathBounds = getStrokeBounds(pathStart, pathEnd, isVertical5);
3982
+ for (const trace of traces) {
3983
+ for (let traceIndex = 0; traceIndex < trace.tracePath.length - 1; traceIndex++) {
3984
+ const traceStart = trace.tracePath[traceIndex];
3985
+ const traceEnd = trace.tracePath[traceIndex + 1];
3986
+ const isParallel = isVertical5 ? Math.abs(traceStart.x - traceEnd.x) < COINCIDENT_EPS : Math.abs(traceStart.y - traceEnd.y) < COINCIDENT_EPS;
3987
+ if (!isParallel) continue;
3988
+ const overlap = boundsIntersection(
3989
+ pathBounds,
3990
+ getStrokeBounds(traceStart, traceEnd, isVertical5)
3991
+ );
3992
+ if (!overlap) continue;
3993
+ const overlapLength = isVertical5 ? overlap.maxY - overlap.minY : overlap.maxX - overlap.minX;
3994
+ if (overlapLength > COINCIDENT_EPS) return true;
3995
+ }
3996
+ }
3997
+ }
3998
+ return false;
3999
+ };
3957
4000
 
3958
4001
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts
3959
4002
  var MAX_TRIES = 5;
@@ -5307,6 +5350,53 @@ var balanceZShapes = ({
5307
5350
  };
5308
5351
  };
5309
5352
 
5353
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts
5354
+ import { boundsIntersection as boundsIntersection2 } from "@tscircuit/math-utils";
5355
+ var getAlongBounds = (orientation, minAlong, maxAlong) => orientation === "vertical" ? { minX: 0, maxX: 0, minY: minAlong, maxY: maxAlong } : { minX: minAlong, maxX: maxAlong, minY: 0, maxY: 0 };
5356
+ var hasPositiveAlongOverlap = (orientation, first, second) => {
5357
+ const overlap = boundsIntersection2(first, second);
5358
+ if (!overlap) return false;
5359
+ const overlapLength = orientation === "vertical" ? overlap.maxY - overlap.minY : overlap.maxX - overlap.minX;
5360
+ return overlapLength > RAIL_ALIGNMENT_EPSILON;
5361
+ };
5362
+ var getRailAlignmentFallbackCoordinates = ({
5363
+ group,
5364
+ originalCoordinates,
5365
+ otherNetTraces
5366
+ }) => {
5367
+ const orientation = group[0].orientation;
5368
+ const groupAlongBounds = group.map(
5369
+ (segment) => getAlongBounds(orientation, segment.minAlong, segment.maxAlong)
5370
+ );
5371
+ const fallbackClearance = SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE;
5372
+ const coordinates = [];
5373
+ for (const trace of otherNetTraces) {
5374
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
5375
+ const start = trace.tracePath[index];
5376
+ const end = trace.tracePath[index + 1];
5377
+ if (getRailOrientation(start, end) !== orientation) continue;
5378
+ const minAlong = orientation === "vertical" ? Math.min(start.y, end.y) : Math.min(start.x, end.x);
5379
+ const maxAlong = orientation === "vertical" ? Math.max(start.y, end.y) : Math.max(start.x, end.x);
5380
+ const traceAlongBounds = getAlongBounds(orientation, minAlong, maxAlong);
5381
+ if (!groupAlongBounds.some(
5382
+ (bounds) => hasPositiveAlongOverlap(orientation, bounds, traceAlongBounds)
5383
+ )) {
5384
+ continue;
5385
+ }
5386
+ const coordinate = orientation === "vertical" ? start.x : start.y;
5387
+ const touchesOriginalCandidate = originalCoordinates.some(
5388
+ (originalCoordinate) => Math.abs(originalCoordinate - coordinate) <= SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE
5389
+ );
5390
+ if (!touchesOriginalCandidate) continue;
5391
+ coordinates.push(
5392
+ coordinate - fallbackClearance,
5393
+ coordinate + fallbackClearance
5394
+ );
5395
+ }
5396
+ }
5397
+ return getDistinctCoordinates(coordinates);
5398
+ };
5399
+
5310
5400
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
5311
5401
  var moveRailSegments = (trace, segments, coordinate) => {
5312
5402
  const pointsToMove = /* @__PURE__ */ new Set();
@@ -5578,7 +5668,7 @@ var evaluateRailGroup = ({
5578
5668
  (trace) => groupTraceIds.has(trace.mspPairId)
5579
5669
  );
5580
5670
  const baseline = getTraceGeometryMetrics(originalGroupTraces, traces);
5581
- const coordinates = getDistinctCoordinates(
5671
+ const originalCoordinates = getDistinctCoordinates(
5582
5672
  group.map((segment) => segment.coordinate)
5583
5673
  );
5584
5674
  const otherNetTraces = traces.filter(
@@ -5587,58 +5677,72 @@ var evaluateRailGroup = ({
5587
5677
  const immutableSameNetTraces = traces.filter(
5588
5678
  (trace) => trace.globalConnNetId === group[0].globalConnNetId && !eligibleTraceIds.has(trace.mspPairId)
5589
5679
  );
5590
- let best = null;
5591
- for (const coordinate of coordinates) {
5592
- const candidateMap = /* @__PURE__ */ new Map();
5593
- for (const trace of originalGroupTraces) {
5594
- const candidateTrace = moveRailSegments(
5595
- trace,
5596
- group.filter((segment) => segment.traceId === trace.mspPairId),
5597
- coordinate
5680
+ const evaluateCoordinates = (coordinates) => {
5681
+ let best = null;
5682
+ for (const coordinate of coordinates) {
5683
+ const candidateMap = /* @__PURE__ */ new Map();
5684
+ for (const trace of originalGroupTraces) {
5685
+ const candidateTrace = moveRailSegments(
5686
+ trace,
5687
+ group.filter((segment) => segment.traceId === trace.mspPairId),
5688
+ coordinate
5689
+ );
5690
+ candidateMap.set(trace.mspPairId, candidateTrace);
5691
+ }
5692
+ const candidateTraces = [...candidateMap.values()];
5693
+ const allCandidateTraces = traces.map(
5694
+ (trace) => candidateMap.get(trace.mspPairId) ?? trace
5598
5695
  );
5599
- candidateMap.set(trace.mspPairId, candidateTrace);
5600
- }
5601
- const candidateTraces = [...candidateMap.values()];
5602
- const allCandidateTraces = traces.map(
5603
- (trace) => candidateMap.get(trace.mspPairId) ?? trace
5604
- );
5605
- const candidatesAreClear = candidateTraces.every(
5606
- (candidate2) => !isPathCollidingWithObstacles(candidate2.tracePath, obstacles) && detectTraceLabelOverlap({
5607
- traces: [candidate2],
5608
- netLabels: netLabelPlacements
5609
- }).length === 0 && !doesPathCoincideWithTraces(candidate2.tracePath, otherNetTraces) && !doesPathCoincideWithTraces(
5610
- candidate2.tracePath,
5611
- immutableSameNetTraces.filter(
5612
- (trace) => trace.mspPairId !== candidate2.mspPairId
5696
+ const candidatesAreClear = candidateTraces.every(
5697
+ (candidate2) => !isPathCollidingWithObstacles(candidate2.tracePath, obstacles) && detectTraceLabelOverlap({
5698
+ traces: [candidate2],
5699
+ netLabels: netLabelPlacements
5700
+ }).length === 0 && !doesPathOverlapTraceStrokes(candidate2.tracePath, otherNetTraces) && !doesPathCoincideWithTraces(
5701
+ candidate2.tracePath,
5702
+ immutableSameNetTraces.filter(
5703
+ (trace) => trace.mspPairId !== candidate2.mspPairId
5704
+ )
5613
5705
  )
5614
- )
5615
- );
5616
- if (!candidatesAreClear) continue;
5617
- if (!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)) {
5618
- continue;
5619
- }
5620
- const metrics = getTraceGeometryMetrics(candidateTraces, allCandidateTraces);
5621
- if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue;
5622
- if (!isReadabilityImprovement(metrics, baseline)) continue;
5623
- const score = {
5624
- ...metrics,
5625
- displacement: group.reduce(
5626
- (sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
5627
- 0
5628
- ),
5629
- coordinate
5630
- };
5631
- const changedTraceIds = candidateTraces.filter((candidate2) => {
5632
- const original = traces.find(
5633
- (trace) => trace.mspPairId === candidate2.mspPairId
5634
5706
  );
5635
- return tracePathChanged(original, candidate2);
5636
- }).map((trace) => trace.mspPairId);
5637
- if (changedTraceIds.length === 0) continue;
5638
- const candidate = { traces: allCandidateTraces, changedTraceIds, score };
5639
- if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate;
5640
- }
5641
- return best;
5707
+ if (!candidatesAreClear) continue;
5708
+ if (!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)) {
5709
+ continue;
5710
+ }
5711
+ const metrics = getTraceGeometryMetrics(
5712
+ candidateTraces,
5713
+ allCandidateTraces
5714
+ );
5715
+ if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue;
5716
+ if (!isReadabilityImprovement(metrics, baseline)) continue;
5717
+ const score = {
5718
+ ...metrics,
5719
+ displacement: group.reduce(
5720
+ (sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
5721
+ 0
5722
+ ),
5723
+ coordinate
5724
+ };
5725
+ const changedTraceIds = candidateTraces.filter((candidate2) => {
5726
+ const original = traces.find(
5727
+ (trace) => trace.mspPairId === candidate2.mspPairId
5728
+ );
5729
+ return tracePathChanged(original, candidate2);
5730
+ }).map((trace) => trace.mspPairId);
5731
+ if (changedTraceIds.length === 0) continue;
5732
+ const candidate = { traces: allCandidateTraces, changedTraceIds, score };
5733
+ if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate;
5734
+ }
5735
+ return best;
5736
+ };
5737
+ const originalCandidate = evaluateCoordinates(originalCoordinates);
5738
+ if (originalCandidate) return originalCandidate;
5739
+ return evaluateCoordinates(
5740
+ getRailAlignmentFallbackCoordinates({
5741
+ group,
5742
+ originalCoordinates,
5743
+ otherNetTraces
5744
+ })
5745
+ );
5642
5746
  };
5643
5747
 
5644
5748
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts
@@ -3,8 +3,12 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
3
3
  import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
4
4
  import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
5
  import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
6
- import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
6
+ import {
7
+ doesPathCoincideWithTraces,
8
+ doesPathOverlapTraceStrokes,
9
+ } from "lib/utils/doesPathCoincideWithTraces"
7
10
  import { getDistinctCoordinates, pointsEqual } from "./geometry"
11
+ import { getRailAlignmentFallbackCoordinates } from "./getRailAlignmentFallbackCoordinates"
8
12
  import { moveRailSegments } from "./moveRailSegments"
9
13
  import { preservesLabelAnchors } from "./preservesLabelAnchors"
10
14
  import {
@@ -43,7 +47,7 @@ export const evaluateRailGroup = ({
43
47
  groupTraceIds.has(trace.mspPairId),
44
48
  )
45
49
  const baseline = getTraceGeometryMetrics(originalGroupTraces, traces)
46
- const coordinates = getDistinctCoordinates(
50
+ const originalCoordinates = getDistinctCoordinates(
47
51
  group.map((segment) => segment.coordinate),
48
52
  )
49
53
  const otherNetTraces = traces.filter(
@@ -55,70 +59,86 @@ export const evaluateRailGroup = ({
55
59
  !eligibleTraceIds.has(trace.mspPairId),
56
60
  )
57
61
 
58
- let best: AlignmentCandidate | null = null
59
- for (const coordinate of coordinates) {
60
- const candidateMap = new Map<string, SolvedTracePath>()
62
+ const evaluateCoordinates = (coordinates: number[]) => {
63
+ let best: AlignmentCandidate | null = null
64
+ for (const coordinate of coordinates) {
65
+ const candidateMap = new Map<string, SolvedTracePath>()
61
66
 
62
- for (const trace of originalGroupTraces) {
63
- const candidateTrace = moveRailSegments(
64
- trace,
65
- group.filter((segment) => segment.traceId === trace.mspPairId),
66
- coordinate,
67
- )
68
- candidateMap.set(trace.mspPairId, candidateTrace)
69
- }
67
+ for (const trace of originalGroupTraces) {
68
+ const candidateTrace = moveRailSegments(
69
+ trace,
70
+ group.filter((segment) => segment.traceId === trace.mspPairId),
71
+ coordinate,
72
+ )
73
+ candidateMap.set(trace.mspPairId, candidateTrace)
74
+ }
70
75
 
71
- const candidateTraces = [...candidateMap.values()]
72
- const allCandidateTraces = traces.map(
73
- (trace) => candidateMap.get(trace.mspPairId) ?? trace,
74
- )
75
- const candidatesAreClear = candidateTraces.every(
76
- (candidate) =>
77
- !isPathCollidingWithObstacles(candidate.tracePath, obstacles) &&
78
- detectTraceLabelOverlap({
79
- traces: [candidate],
80
- netLabels: netLabelPlacements,
81
- }).length === 0 &&
82
- !doesPathCoincideWithTraces(candidate.tracePath, otherNetTraces) &&
83
- !doesPathCoincideWithTraces(
84
- candidate.tracePath,
85
- immutableSameNetTraces.filter(
86
- (trace) => trace.mspPairId !== candidate.mspPairId,
76
+ const candidateTraces = [...candidateMap.values()]
77
+ const allCandidateTraces = traces.map(
78
+ (trace) => candidateMap.get(trace.mspPairId) ?? trace,
79
+ )
80
+ const candidatesAreClear = candidateTraces.every(
81
+ (candidate) =>
82
+ !isPathCollidingWithObstacles(candidate.tracePath, obstacles) &&
83
+ detectTraceLabelOverlap({
84
+ traces: [candidate],
85
+ netLabels: netLabelPlacements,
86
+ }).length === 0 &&
87
+ !doesPathOverlapTraceStrokes(candidate.tracePath, otherNetTraces) &&
88
+ !doesPathCoincideWithTraces(
89
+ candidate.tracePath,
90
+ immutableSameNetTraces.filter(
91
+ (trace) => trace.mspPairId !== candidate.mspPairId,
92
+ ),
87
93
  ),
88
- ),
89
- )
90
- if (!candidatesAreClear) continue
91
- if (
92
- !preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)
93
- ) {
94
- continue
95
- }
94
+ )
95
+ if (!candidatesAreClear) continue
96
+ if (
97
+ !preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)
98
+ ) {
99
+ continue
100
+ }
96
101
 
97
- const metrics = getTraceGeometryMetrics(candidateTraces, allCandidateTraces)
98
- if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
99
- if (!isReadabilityImprovement(metrics, baseline)) continue
102
+ const metrics = getTraceGeometryMetrics(
103
+ candidateTraces,
104
+ allCandidateTraces,
105
+ )
106
+ if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
107
+ if (!isReadabilityImprovement(metrics, baseline)) continue
100
108
 
101
- const score: AlignmentScore = {
102
- ...metrics,
103
- displacement: group.reduce(
104
- (sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
105
- 0,
106
- ),
107
- coordinate,
109
+ const score: AlignmentScore = {
110
+ ...metrics,
111
+ displacement: group.reduce(
112
+ (sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
113
+ 0,
114
+ ),
115
+ coordinate,
116
+ }
117
+ const changedTraceIds = candidateTraces
118
+ .filter((candidate) => {
119
+ const original = traces.find(
120
+ (trace) => trace.mspPairId === candidate.mspPairId,
121
+ )!
122
+ return tracePathChanged(original, candidate)
123
+ })
124
+ .map((trace) => trace.mspPairId)
125
+ if (changedTraceIds.length === 0) continue
126
+
127
+ const candidate = { traces: allCandidateTraces, changedTraceIds, score }
128
+ if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
108
129
  }
109
- const changedTraceIds = candidateTraces
110
- .filter((candidate) => {
111
- const original = traces.find(
112
- (trace) => trace.mspPairId === candidate.mspPairId,
113
- )!
114
- return tracePathChanged(original, candidate)
115
- })
116
- .map((trace) => trace.mspPairId)
117
- if (changedTraceIds.length === 0) continue
118
130
 
119
- const candidate = { traces: allCandidateTraces, changedTraceIds, score }
120
- if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
131
+ return best
121
132
  }
122
133
 
123
- return best
134
+ const originalCandidate = evaluateCoordinates(originalCoordinates)
135
+ if (originalCandidate) return originalCandidate
136
+
137
+ return evaluateCoordinates(
138
+ getRailAlignmentFallbackCoordinates({
139
+ group,
140
+ originalCoordinates,
141
+ otherNetTraces,
142
+ }),
143
+ )
124
144
  }
@@ -0,0 +1,96 @@
1
+ import { boundsIntersection, type Bounds } from "@tscircuit/math-utils"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import {
4
+ SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE,
5
+ SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE,
6
+ } from "lib/utils/doesPathCoincideWithTraces"
7
+ import {
8
+ getDistinctCoordinates,
9
+ getRailOrientation,
10
+ RAIL_ALIGNMENT_EPSILON,
11
+ } from "./geometry"
12
+ import type { RailOrientation, RailSegment } from "./types"
13
+
14
+ const getAlongBounds = (
15
+ orientation: RailOrientation,
16
+ minAlong: number,
17
+ maxAlong: number,
18
+ ): Bounds =>
19
+ orientation === "vertical"
20
+ ? { minX: 0, maxX: 0, minY: minAlong, maxY: maxAlong }
21
+ : { minX: minAlong, maxX: maxAlong, minY: 0, maxY: 0 }
22
+
23
+ const hasPositiveAlongOverlap = (
24
+ orientation: RailOrientation,
25
+ first: Bounds,
26
+ second: Bounds,
27
+ ) => {
28
+ const overlap = boundsIntersection(first, second)
29
+ if (!overlap) return false
30
+ const overlapLength =
31
+ orientation === "vertical"
32
+ ? overlap.maxY - overlap.minY
33
+ : overlap.maxX - overlap.minX
34
+ return overlapLength > RAIL_ALIGNMENT_EPSILON
35
+ }
36
+
37
+ /**
38
+ * Derives alternatives only for original alignment coordinates whose rendered
39
+ * stroke touches a parallel segment from another net.
40
+ */
41
+ export const getRailAlignmentFallbackCoordinates = ({
42
+ group,
43
+ originalCoordinates,
44
+ otherNetTraces,
45
+ }: {
46
+ group: RailSegment[]
47
+ originalCoordinates: number[]
48
+ otherNetTraces: SolvedTracePath[]
49
+ }) => {
50
+ const orientation = group[0]!.orientation
51
+ const groupAlongBounds = group.map((segment) =>
52
+ getAlongBounds(orientation, segment.minAlong, segment.maxAlong),
53
+ )
54
+ const fallbackClearance = SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE
55
+ const coordinates: number[] = []
56
+
57
+ for (const trace of otherNetTraces) {
58
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
59
+ const start = trace.tracePath[index]!
60
+ const end = trace.tracePath[index + 1]!
61
+ if (getRailOrientation(start, end) !== orientation) continue
62
+
63
+ const minAlong =
64
+ orientation === "vertical"
65
+ ? Math.min(start.y, end.y)
66
+ : Math.min(start.x, end.x)
67
+ const maxAlong =
68
+ orientation === "vertical"
69
+ ? Math.max(start.y, end.y)
70
+ : Math.max(start.x, end.x)
71
+ const traceAlongBounds = getAlongBounds(orientation, minAlong, maxAlong)
72
+ if (
73
+ !groupAlongBounds.some((bounds) =>
74
+ hasPositiveAlongOverlap(orientation, bounds, traceAlongBounds),
75
+ )
76
+ ) {
77
+ continue
78
+ }
79
+
80
+ const coordinate = orientation === "vertical" ? start.x : start.y
81
+ const touchesOriginalCandidate = originalCoordinates.some(
82
+ (originalCoordinate) =>
83
+ Math.abs(originalCoordinate - coordinate) <=
84
+ SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE,
85
+ )
86
+ if (!touchesOriginalCandidate) continue
87
+
88
+ coordinates.push(
89
+ coordinate - fallbackClearance,
90
+ coordinate + fallbackClearance,
91
+ )
92
+ }
93
+ }
94
+
95
+ return getDistinctCoordinates(coordinates)
96
+ }
@@ -1,7 +1,18 @@
1
- import type { Point } from "@tscircuit/math-utils"
1
+ import { boundsIntersection, type Point } from "@tscircuit/math-utils"
2
2
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
3
 
4
4
  const COINCIDENT_EPS = 2e-3
5
+ const GEOMETRY_EPS = 1e-6
6
+
7
+ // Schematic traces are rendered 0.02 schematic units wide.
8
+ export const SCHEMATIC_TRACE_STROKE_WIDTH = 0.02
9
+ export const SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE =
10
+ SCHEMATIC_TRACE_STROKE_WIDTH + GEOMETRY_EPS
11
+ // Rail-alignment fallbacks need a visible gap, not only non-overlapping trace
12
+ // bodies. Three stroke widths of centerline separation leave two stroke widths
13
+ // of whitespace between the rendered traces.
14
+ export const SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE =
15
+ SCHEMATIC_TRACE_STROKE_WIDTH * 3
5
16
 
6
17
  /**
7
18
  * Returns true when an orthogonal path shares a positive-length segment with
@@ -59,3 +70,62 @@ export const doesPathCoincideWithTraces = (
59
70
 
60
71
  return false
61
72
  }
73
+
74
+ /** Returns true when the rendered strokes of parallel trace runs overlap. */
75
+ export const doesPathOverlapTraceStrokes = (
76
+ path: Point[],
77
+ traces: SolvedTracePath[],
78
+ ): boolean => {
79
+ const traceStrokeRadius = SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE / 2
80
+ const getStrokeBounds = (start: Point, end: Point, isVertical: boolean) =>
81
+ isVertical
82
+ ? {
83
+ minX: start.x - traceStrokeRadius,
84
+ maxX: start.x + traceStrokeRadius,
85
+ minY: Math.min(start.y, end.y),
86
+ maxY: Math.max(start.y, end.y),
87
+ }
88
+ : {
89
+ minX: Math.min(start.x, end.x),
90
+ maxX: Math.max(start.x, end.x),
91
+ minY: start.y - traceStrokeRadius,
92
+ maxY: start.y + traceStrokeRadius,
93
+ }
94
+
95
+ for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
96
+ const pathStart = path[pathIndex]!
97
+ const pathEnd = path[pathIndex + 1]!
98
+ const isVertical = Math.abs(pathStart.x - pathEnd.x) < COINCIDENT_EPS
99
+ const isHorizontal = Math.abs(pathStart.y - pathEnd.y) < COINCIDENT_EPS
100
+ if (!isVertical && !isHorizontal) continue
101
+
102
+ const pathBounds = getStrokeBounds(pathStart, pathEnd, isVertical)
103
+ for (const trace of traces) {
104
+ for (
105
+ let traceIndex = 0;
106
+ traceIndex < trace.tracePath.length - 1;
107
+ traceIndex++
108
+ ) {
109
+ const traceStart = trace.tracePath[traceIndex]!
110
+ const traceEnd = trace.tracePath[traceIndex + 1]!
111
+ const isParallel = isVertical
112
+ ? Math.abs(traceStart.x - traceEnd.x) < COINCIDENT_EPS
113
+ : Math.abs(traceStart.y - traceEnd.y) < COINCIDENT_EPS
114
+ if (!isParallel) continue
115
+
116
+ const overlap = boundsIntersection(
117
+ pathBounds,
118
+ getStrokeBounds(traceStart, traceEnd, isVertical),
119
+ )
120
+ if (!overlap) continue
121
+
122
+ const overlapLength = isVertical
123
+ ? overlap.maxY - overlap.minY
124
+ : overlap.maxX - overlap.minX
125
+ if (overlapLength > COINCIDENT_EPS) return true
126
+ }
127
+ }
128
+ }
129
+
130
+ return false
131
+ }
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.120",
4
+ "version": "0.0.121",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",