@tscircuit/schematic-trace-solver 0.0.97 → 0.0.99

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 (50) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.js +1077 -519
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +30 -0
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
  6. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +45 -1
  7. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +55 -13
  8. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +66 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +124 -0
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +108 -0
  11. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +121 -0
  12. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +114 -0
  13. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +27 -0
  14. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts +30 -0
  15. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/scoreRailAlignment.ts +67 -0
  16. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/types.ts +34 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -60
  18. package/lib/utils/doesPathCoincideWithTraces.ts +61 -0
  19. package/package.json +1 -1
  20. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
  21. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
  22. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
  23. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
  24. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -1
  25. package/tests/examples/__snapshots__/example02.snap.svg +1 -1
  26. package/tests/examples/__snapshots__/example09.snap.svg +9 -13
  27. package/tests/examples/__snapshots__/example14.snap.svg +1 -1
  28. package/tests/examples/__snapshots__/example18.snap.svg +1 -1
  29. package/tests/examples/__snapshots__/example21.snap.svg +30 -32
  30. package/tests/examples/__snapshots__/example29.snap.svg +13 -19
  31. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  32. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -1
  33. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +1 -1
  34. package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
  35. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
  36. package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
  37. package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +24 -0
  38. package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
  39. package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
  40. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +38 -0
  41. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-component-scope.test.ts +38 -0
  42. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-eligible-traces.test.ts +28 -0
  43. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-horizontal.test.ts +62 -0
  44. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-anchor.test.ts +36 -0
  45. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-junction.test.ts +42 -0
  46. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-obstacle.test.ts +28 -0
  47. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +52 -0
  48. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-vertical.test.ts +25 -0
  49. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-visible-length.test.ts +40 -0
  50. package/tests/solvers/TraceCleanupSolver/fixtures/alignSameNetRails.ts +89 -0
@@ -0,0 +1,108 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import type { RailOrientation, RailSegment } from "./types"
4
+
5
+ export const RAIL_ALIGNMENT_EPSILON = 2e-3
6
+
7
+ export const nearlyEqual = (a: number, b: number) =>
8
+ Math.abs(a - b) < RAIL_ALIGNMENT_EPSILON
9
+
10
+ export const isHorizontal = (a: Point, b: Point) => nearlyEqual(a.y, b.y)
11
+
12
+ export const isVertical = (a: Point, b: Point) => nearlyEqual(a.x, b.x)
13
+
14
+ const isPositiveLength = (a: Point, b: Point) =>
15
+ Math.abs(a.x - b.x) >= RAIL_ALIGNMENT_EPSILON ||
16
+ Math.abs(a.y - b.y) >= RAIL_ALIGNMENT_EPSILON
17
+
18
+ export const getRailOrientation = (
19
+ a: Point,
20
+ b: Point,
21
+ ): RailOrientation | null => {
22
+ if (!isPositiveLength(a, b)) return null
23
+ if (isVertical(a, b)) return "vertical"
24
+ if (isHorizontal(a, b)) return "horizontal"
25
+ return null
26
+ }
27
+
28
+ export const rangesTouchOrOverlap = (a: RailSegment, b: RailSegment) =>
29
+ Math.min(a.maxAlong, b.maxAlong) - Math.max(a.minAlong, b.minAlong) >=
30
+ -RAIL_ALIGNMENT_EPSILON
31
+
32
+ export const pointsEqual = (a: Point, b: Point) =>
33
+ nearlyEqual(a.x, b.x) && nearlyEqual(a.y, b.y)
34
+
35
+ export const getDistinctCoordinates = (coordinates: number[]) => {
36
+ const distinct: number[] = []
37
+ for (const coordinate of coordinates) {
38
+ if (!distinct.some((item) => nearlyEqual(item, coordinate))) {
39
+ distinct.push(coordinate)
40
+ }
41
+ }
42
+ return distinct
43
+ }
44
+
45
+ interface Interval {
46
+ coordinate: number
47
+ min: number
48
+ max: number
49
+ }
50
+
51
+ const getMergedIntervalLength = (intervals: Interval[]) => {
52
+ const groups: Interval[][] = []
53
+ for (const interval of intervals) {
54
+ const group = groups.find((items) =>
55
+ nearlyEqual(items[0]!.coordinate, interval.coordinate),
56
+ )
57
+ if (group) group.push(interval)
58
+ else groups.push([interval])
59
+ }
60
+
61
+ return groups.reduce((total, group) => {
62
+ const sorted = [...group].sort((a, b) => a.min - b.min)
63
+ let groupLength = 0
64
+ let currentMin = sorted[0]!.min
65
+ let currentMax = sorted[0]!.max
66
+
67
+ for (const interval of sorted.slice(1)) {
68
+ if (interval.min <= currentMax + RAIL_ALIGNMENT_EPSILON) {
69
+ currentMax = Math.max(currentMax, interval.max)
70
+ } else {
71
+ groupLength += currentMax - currentMin
72
+ currentMin = interval.min
73
+ currentMax = interval.max
74
+ }
75
+ }
76
+
77
+ return total + groupLength + currentMax - currentMin
78
+ }, 0)
79
+ }
80
+
81
+ /** Returns rendered length after overlapping collinear runs are merged. */
82
+ export const getVisibleTraceLength = (traces: SolvedTracePath[]) => {
83
+ const horizontal: Interval[] = []
84
+ const vertical: Interval[] = []
85
+
86
+ for (const trace of traces) {
87
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
88
+ const start = trace.tracePath[index]!
89
+ const end = trace.tracePath[index + 1]!
90
+
91
+ if (isHorizontal(start, end)) {
92
+ horizontal.push({
93
+ coordinate: start.y,
94
+ min: Math.min(start.x, end.x),
95
+ max: Math.max(start.x, end.x),
96
+ })
97
+ } else if (isVertical(start, end)) {
98
+ vertical.push({
99
+ coordinate: start.x,
100
+ min: Math.min(start.y, end.y),
101
+ max: Math.max(start.y, end.y),
102
+ })
103
+ }
104
+ }
105
+ }
106
+
107
+ return getMergedIntervalLength(horizontal) + getMergedIntervalLength(vertical)
108
+ }
@@ -0,0 +1,121 @@
1
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
3
+ import type { InputChip } from "lib/types/InputProblem"
4
+ import type { FacingDirection } from "lib/utils/dir"
5
+ import { getRailOrientation, RAIL_ALIGNMENT_EPSILON } from "./geometry"
6
+ import type { RailSegment } from "./types"
7
+
8
+ type UnassociatedRailSegment = Omit<
9
+ RailSegment,
10
+ "componentId" | "componentFacingDirection"
11
+ >
12
+
13
+ const getMovableRailSegments = (
14
+ trace: SolvedTracePath,
15
+ ): UnassociatedRailSegment[] => {
16
+ const segments: UnassociatedRailSegment[] = []
17
+ const path = trace.tracePath
18
+
19
+ for (let segmentIndex = 1; segmentIndex < path.length - 2; segmentIndex++) {
20
+ const previous = path[segmentIndex - 1]!
21
+ const start = path[segmentIndex]!
22
+ const end = path[segmentIndex + 1]!
23
+ const next = path[segmentIndex + 2]!
24
+ const orientation = getRailOrientation(start, end)
25
+ if (!orientation) continue
26
+
27
+ const previousOrientation = getRailOrientation(previous, start)
28
+ const nextOrientation = getRailOrientation(end, next)
29
+ if (!previousOrientation || !nextOrientation) continue
30
+ if (previousOrientation === orientation || nextOrientation === orientation)
31
+ continue
32
+
33
+ const alongValues =
34
+ orientation === "vertical" ? [start.y, end.y] : [start.x, end.x]
35
+ segments.push({
36
+ traceId: trace.mspPairId,
37
+ segmentIndex,
38
+ globalConnNetId: trace.globalConnNetId,
39
+ orientation,
40
+ coordinate: orientation === "vertical" ? start.x : start.y,
41
+ minAlong: Math.min(...alongValues),
42
+ maxAlong: Math.max(...alongValues),
43
+ })
44
+ }
45
+
46
+ return segments
47
+ }
48
+
49
+ const railIsOutsideComponent = (
50
+ segment: UnassociatedRailSegment,
51
+ chip: InputChip,
52
+ facingDirection: FacingDirection,
53
+ ) => {
54
+ const minX = chip.center.x - chip.width / 2
55
+ const maxX = chip.center.x + chip.width / 2
56
+ const minY = chip.center.y - chip.height / 2
57
+ const maxY = chip.center.y + chip.height / 2
58
+
59
+ switch (facingDirection) {
60
+ case "x-":
61
+ return (
62
+ segment.orientation === "vertical" &&
63
+ segment.coordinate <= minX + RAIL_ALIGNMENT_EPSILON
64
+ )
65
+ case "x+":
66
+ return (
67
+ segment.orientation === "vertical" &&
68
+ segment.coordinate >= maxX - RAIL_ALIGNMENT_EPSILON
69
+ )
70
+ case "y+":
71
+ return (
72
+ segment.orientation === "horizontal" &&
73
+ segment.coordinate >= maxY - RAIL_ALIGNMENT_EPSILON
74
+ )
75
+ case "y-":
76
+ return (
77
+ segment.orientation === "horizontal" &&
78
+ segment.coordinate <= minY + RAIL_ALIGNMENT_EPSILON
79
+ )
80
+ }
81
+ }
82
+
83
+ /** Associates each movable internal rail with the nearest component endpoint. */
84
+ export const getComponentSideRailSegments = (
85
+ trace: SolvedTracePath,
86
+ chipMap: Map<string, InputChip>,
87
+ ): RailSegment[] => {
88
+ const segments: RailSegment[] = []
89
+
90
+ for (const segment of getMovableRailSegments(trace)) {
91
+ const associations = trace.pins.flatMap((pin, pinIndex) => {
92
+ const chip = chipMap.get(pin.chipId)
93
+ if (!chip) return []
94
+
95
+ const componentFacingDirection =
96
+ pin._facingDirection ?? getPinDirection(pin, chip)
97
+ if (!railIsOutsideComponent(segment, chip, componentFacingDirection)) {
98
+ return []
99
+ }
100
+
101
+ return [
102
+ {
103
+ componentId: chip.chipId,
104
+ componentFacingDirection,
105
+ distanceFromEndpoint:
106
+ pinIndex === 0
107
+ ? segment.segmentIndex
108
+ : trace.tracePath.length - 2 - segment.segmentIndex,
109
+ },
110
+ ]
111
+ })
112
+
113
+ associations.sort((a, b) => a.distanceFromEndpoint - b.distanceFromEndpoint)
114
+ const association = associations[0]
115
+ if (!association) continue
116
+
117
+ segments.push({ ...segment, ...association })
118
+ }
119
+
120
+ return segments
121
+ }
@@ -0,0 +1,114 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
4
+ import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+ import { getComponentSideRailSegments } from "./getComponentSideRailSegments"
7
+ import { nearlyEqual, rangesTouchOrOverlap } from "./geometry"
8
+ import type { RailSegment } from "./types"
9
+
10
+ const getCorridor = (a: RailSegment, b: RailSegment): [Point, Point] => {
11
+ const overlapMin = Math.max(a.minAlong, b.minAlong)
12
+ const overlapMax = Math.min(a.maxAlong, b.maxAlong)
13
+ const along = (overlapMin + overlapMax) / 2
14
+
15
+ return a.orientation === "vertical"
16
+ ? [
17
+ { x: a.coordinate, y: along },
18
+ { x: b.coordinate, y: along },
19
+ ]
20
+ : [
21
+ { x: along, y: a.coordinate },
22
+ { x: along, y: b.coordinate },
23
+ ]
24
+ }
25
+
26
+ const corridorIsClear = (
27
+ a: RailSegment,
28
+ b: RailSegment,
29
+ obstacles: ObstacleRect[],
30
+ ) => {
31
+ if (!rangesTouchOrOverlap(a, b)) return true
32
+ const [start, end] = getCorridor(a, b)
33
+ return !obstacles.some((obstacle) =>
34
+ segmentIntersectsRect(start, end, obstacle),
35
+ )
36
+ }
37
+
38
+ const tracesSharePin = (
39
+ a: RailSegment,
40
+ b: RailSegment,
41
+ traceMap: Map<string, SolvedTracePath>,
42
+ ) => {
43
+ if (a.traceId === b.traceId) return true
44
+
45
+ const aPinIds = new Set(traceMap.get(a.traceId)!.pins.map((pin) => pin.pinId))
46
+ return traceMap.get(b.traceId)!.pins.some((pin) => aPinIds.has(pin.pinId))
47
+ }
48
+
49
+ const canJoinRailGroup = (
50
+ start: RailSegment,
51
+ current: RailSegment,
52
+ candidate: RailSegment,
53
+ traceMap: Map<string, SolvedTracePath>,
54
+ obstacles: ObstacleRect[],
55
+ ) =>
56
+ candidate.globalConnNetId === start.globalConnNetId &&
57
+ candidate.orientation === start.orientation &&
58
+ candidate.componentId === start.componentId &&
59
+ candidate.componentFacingDirection === start.componentFacingDirection &&
60
+ (rangesTouchOrOverlap(current, candidate) ||
61
+ tracesSharePin(current, candidate, traceMap)) &&
62
+ corridorIsClear(current, candidate, obstacles)
63
+
64
+ export const getRailGroups = (
65
+ traces: SolvedTracePath[],
66
+ eligibleTraceIds: ReadonlySet<string>,
67
+ inputProblem: InputProblem,
68
+ obstacles: ObstacleRect[],
69
+ ): RailSegment[][] => {
70
+ const chipMap = new Map(inputProblem.chips.map((chip) => [chip.chipId, chip]))
71
+ const segments = traces
72
+ .filter((trace) => eligibleTraceIds.has(trace.mspPairId))
73
+ .flatMap((trace) => getComponentSideRailSegments(trace, chipMap))
74
+ const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]))
75
+ const visited = new Set<number>()
76
+ const groups: RailSegment[][] = []
77
+
78
+ for (let startIndex = 0; startIndex < segments.length; startIndex++) {
79
+ if (visited.has(startIndex)) continue
80
+
81
+ const start = segments[startIndex]!
82
+ const queue = [startIndex]
83
+ const group: RailSegment[] = []
84
+ visited.add(startIndex)
85
+
86
+ for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
87
+ const current = segments[queue[queueIndex]!]!
88
+ group.push(current)
89
+
90
+ for (
91
+ let candidateIndex = 0;
92
+ candidateIndex < segments.length;
93
+ candidateIndex++
94
+ ) {
95
+ if (visited.has(candidateIndex)) continue
96
+ const candidate = segments[candidateIndex]!
97
+ if (!canJoinRailGroup(start, current, candidate, traceMap, obstacles)) {
98
+ continue
99
+ }
100
+
101
+ visited.add(candidateIndex)
102
+ queue.push(candidateIndex)
103
+ }
104
+ }
105
+
106
+ const traceCount = new Set(group.map((segment) => segment.traceId)).size
107
+ const hasDifferentCoordinates = group.some(
108
+ (segment) => !nearlyEqual(segment.coordinate, group[0]!.coordinate),
109
+ )
110
+ if (traceCount >= 2 && hasDifferentCoordinates) groups.push(group)
111
+ }
112
+
113
+ return groups
114
+ }
@@ -0,0 +1,27 @@
1
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import { simplifyPath } from "../simplifyPath"
3
+ import type { RailSegment } from "./types"
4
+
5
+ export const moveRailSegments = (
6
+ trace: SolvedTracePath,
7
+ segments: RailSegment[],
8
+ coordinate: number,
9
+ ): SolvedTracePath => {
10
+ const pointsToMove = new Set<number>()
11
+ for (const segment of segments) {
12
+ pointsToMove.add(segment.segmentIndex)
13
+ pointsToMove.add(segment.segmentIndex + 1)
14
+ }
15
+
16
+ const orientation = segments[0]!.orientation
17
+ const tracePath = simplifyPath(
18
+ trace.tracePath.map((point, index) => {
19
+ if (!pointsToMove.has(index)) return point
20
+ return orientation === "vertical"
21
+ ? { ...point, x: coordinate }
22
+ : { ...point, y: coordinate }
23
+ }),
24
+ )
25
+
26
+ return { ...trace, tracePath }
27
+ }
@@ -0,0 +1,30 @@
1
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
2
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+
5
+ const getAnchoredTraceIds = (
6
+ label: NetLabelPlacement,
7
+ traces: SolvedTracePath[],
8
+ ) =>
9
+ new Set(
10
+ traces
11
+ .filter(
12
+ (trace) =>
13
+ trace.globalConnNetId === label.globalConnNetId &&
14
+ tracePathContainsPoint(trace.tracePath, label.anchorPoint),
15
+ )
16
+ .map((trace) => trace.mspPairId),
17
+ )
18
+
19
+ export const preservesLabelAnchors = (
20
+ labels: NetLabelPlacement[],
21
+ before: SolvedTracePath[],
22
+ after: SolvedTracePath[],
23
+ ) =>
24
+ labels.every((label) => {
25
+ const anchoredBefore = getAnchoredTraceIds(label, before)
26
+ if (anchoredBefore.size === 0) return true
27
+
28
+ const anchoredAfter = getAnchoredTraceIds(label, after)
29
+ return [...anchoredBefore].every((traceId) => anchoredAfter.has(traceId))
30
+ })
@@ -0,0 +1,67 @@
1
+ import {
2
+ countPathIntersections,
3
+ getPathLength,
4
+ } from "lib/solvers/Example28Solver/geometry"
5
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import { countTurns } from "../countTurns"
7
+ import {
8
+ getVisibleTraceLength,
9
+ nearlyEqual,
10
+ RAIL_ALIGNMENT_EPSILON,
11
+ } from "./geometry"
12
+ import type { AlignmentScore, TraceGeometryMetrics } from "./types"
13
+
14
+ export const countOtherNetCrossings = (
15
+ traces: SolvedTracePath[],
16
+ allTraces: SolvedTracePath[],
17
+ ) => {
18
+ let crossings = 0
19
+ for (const trace of traces) {
20
+ for (const otherTrace of allTraces) {
21
+ if (trace.globalConnNetId === otherTrace.globalConnNetId) continue
22
+ crossings += countPathIntersections(trace.tracePath, otherTrace.tracePath)
23
+ }
24
+ }
25
+ return crossings
26
+ }
27
+
28
+ export const getTraceGeometryMetrics = (
29
+ traces: SolvedTracePath[],
30
+ allTraces: SolvedTracePath[],
31
+ ): TraceGeometryMetrics => ({
32
+ turnCount: traces.reduce(
33
+ (total, trace) => total + countTurns(trace.tracePath),
34
+ 0,
35
+ ),
36
+ visibleLength: getVisibleTraceLength(traces),
37
+ pathLength: traces.reduce(
38
+ (sum, trace) => sum + getPathLength(trace.tracePath),
39
+ 0,
40
+ ),
41
+ otherNetCrossings: countOtherNetCrossings(traces, allTraces),
42
+ })
43
+
44
+ export const scoreIsBetter = (
45
+ candidate: AlignmentScore,
46
+ best: AlignmentScore,
47
+ ) => {
48
+ if (candidate.turnCount !== best.turnCount)
49
+ return candidate.turnCount < best.turnCount
50
+ if (!nearlyEqual(candidate.visibleLength, best.visibleLength))
51
+ return candidate.visibleLength < best.visibleLength
52
+ if (!nearlyEqual(candidate.pathLength, best.pathLength))
53
+ return candidate.pathLength < best.pathLength
54
+ if (candidate.otherNetCrossings !== best.otherNetCrossings)
55
+ return candidate.otherNetCrossings < best.otherNetCrossings
56
+ if (!nearlyEqual(candidate.displacement, best.displacement))
57
+ return candidate.displacement < best.displacement
58
+ return candidate.coordinate < best.coordinate
59
+ }
60
+
61
+ export const isReadabilityImprovement = (
62
+ candidate: TraceGeometryMetrics,
63
+ baseline: TraceGeometryMetrics,
64
+ ) =>
65
+ candidate.turnCount <= baseline.turnCount &&
66
+ (candidate.turnCount < baseline.turnCount ||
67
+ candidate.visibleLength <= baseline.visibleLength + RAIL_ALIGNMENT_EPSILON)
@@ -0,0 +1,34 @@
1
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import type { FacingDirection } from "lib/utils/dir"
3
+
4
+ export type RailOrientation = "horizontal" | "vertical"
5
+
6
+ export interface RailSegment {
7
+ traceId: string
8
+ segmentIndex: number
9
+ globalConnNetId: string
10
+ orientation: RailOrientation
11
+ coordinate: number
12
+ minAlong: number
13
+ maxAlong: number
14
+ componentId: string
15
+ componentFacingDirection: FacingDirection
16
+ }
17
+
18
+ export interface TraceGeometryMetrics {
19
+ turnCount: number
20
+ visibleLength: number
21
+ pathLength: number
22
+ otherNetCrossings: number
23
+ }
24
+
25
+ export interface AlignmentScore extends TraceGeometryMetrics {
26
+ displacement: number
27
+ coordinate: number
28
+ }
29
+
30
+ export interface AlignmentCandidate {
31
+ traces: SolvedTracePath[]
32
+ changedTraceIds: string[]
33
+ score: AlignmentScore
34
+ }
@@ -10,6 +10,7 @@ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/
10
10
  import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
11
11
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
12
12
  import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
13
+ import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
13
14
 
14
15
  interface SingleOverlapSolverInput {
15
16
  trace: SolvedTracePath
@@ -21,66 +22,6 @@ interface SingleOverlapSolverInput {
21
22
  }
22
23
 
23
24
  const MAX_TRIES = 5
24
- const COINCIDENT_EPS = 2e-3
25
-
26
- /**
27
- * Returns true if any segment of the path is parallel and coincident
28
- * (within COINCIDENT_EPS) with a segment of one of the given traces.
29
- * Perpendicular crossings are allowed.
30
- */
31
- const doesPathCoincideWithTraces = (
32
- path: Point[],
33
- traces: SolvedTracePath[],
34
- ): boolean => {
35
- const rangesOverlap1D = (a1: number, a2: number, b1: number, b2: number) =>
36
- Math.min(Math.max(a1, a2), Math.max(b1, b2)) -
37
- Math.max(Math.min(a1, a2), Math.min(b1, b2)) >
38
- COINCIDENT_EPS
39
-
40
- for (let i = 0; i < path.length - 1; i++) {
41
- const pathSegStart = path[i]!
42
- const pathSegEnd = path[i + 1]!
43
- const isVertical = Math.abs(pathSegStart.x - pathSegEnd.x) < COINCIDENT_EPS
44
- const isHorizontal =
45
- Math.abs(pathSegStart.y - pathSegEnd.y) < COINCIDENT_EPS
46
- if (!isVertical && !isHorizontal) continue
47
-
48
- // For a vertical segment, coincidence is measured in x and overlap in y
49
- // (and vice versa for horizontal)
50
- const crossAxis = isVertical ? "x" : "y"
51
- const alongAxis = isVertical ? "y" : "x"
52
-
53
- for (const trace of traces) {
54
- for (let j = 0; j < trace.tracePath.length - 1; j++) {
55
- const traceSegStart = trace.tracePath[j]!
56
- const traceSegEnd = trace.tracePath[j + 1]!
57
-
58
- const isParallel =
59
- Math.abs(traceSegStart[crossAxis] - traceSegEnd[crossAxis]) <
60
- COINCIDENT_EPS
61
- if (!isParallel) continue
62
-
63
- const isCoincident =
64
- Math.abs(pathSegStart[crossAxis] - traceSegStart[crossAxis]) <
65
- COINCIDENT_EPS
66
- if (!isCoincident) continue
67
-
68
- if (
69
- rangesOverlap1D(
70
- pathSegStart[alongAxis],
71
- pathSegEnd[alongAxis],
72
- traceSegStart[alongAxis],
73
- traceSegEnd[alongAxis],
74
- )
75
- ) {
76
- return true
77
- }
78
- }
79
- }
80
- }
81
- return false
82
- }
83
-
84
25
  /**
85
26
  * This solver attempts to find a valid rerouting for a single trace that is
86
27
  * overlapping with a net label. It tries various candidate paths until it
@@ -0,0 +1,61 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+
4
+ const COINCIDENT_EPS = 2e-3
5
+
6
+ /**
7
+ * Returns true when an orthogonal path shares a positive-length segment with
8
+ * an existing trace. Perpendicular crossings and point-only contact are
9
+ * intentionally allowed.
10
+ */
11
+ export const doesPathCoincideWithTraces = (
12
+ path: Point[],
13
+ traces: SolvedTracePath[],
14
+ ): boolean => {
15
+ const rangesOverlap1D = (a1: number, a2: number, b1: number, b2: number) =>
16
+ Math.min(Math.max(a1, a2), Math.max(b1, b2)) -
17
+ Math.max(Math.min(a1, a2), Math.min(b1, b2)) >
18
+ COINCIDENT_EPS
19
+
20
+ for (let i = 0; i < path.length - 1; i++) {
21
+ const pathSegStart = path[i]!
22
+ const pathSegEnd = path[i + 1]!
23
+ const isVertical = Math.abs(pathSegStart.x - pathSegEnd.x) < COINCIDENT_EPS
24
+ const isHorizontal =
25
+ Math.abs(pathSegStart.y - pathSegEnd.y) < COINCIDENT_EPS
26
+ if (!isVertical && !isHorizontal) continue
27
+
28
+ const crossAxis = isVertical ? "x" : "y"
29
+ const alongAxis = isVertical ? "y" : "x"
30
+
31
+ for (const trace of traces) {
32
+ for (let j = 0; j < trace.tracePath.length - 1; j++) {
33
+ const traceSegStart = trace.tracePath[j]!
34
+ const traceSegEnd = trace.tracePath[j + 1]!
35
+
36
+ const isParallel =
37
+ Math.abs(traceSegStart[crossAxis] - traceSegEnd[crossAxis]) <
38
+ COINCIDENT_EPS
39
+ if (!isParallel) continue
40
+
41
+ const isCoincident =
42
+ Math.abs(pathSegStart[crossAxis] - traceSegStart[crossAxis]) <
43
+ COINCIDENT_EPS
44
+ if (!isCoincident) continue
45
+
46
+ if (
47
+ rangesOverlap1D(
48
+ pathSegStart[alongAxis],
49
+ pathSegEnd[alongAxis],
50
+ traceSegStart[alongAxis],
51
+ traceSegEnd[alongAxis],
52
+ )
53
+ ) {
54
+ return true
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ return false
61
+ }
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.97",
4
+ "version": "0.0.99",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",