@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.
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1077 -519
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +30 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +45 -1
- package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +55 -13
- package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +66 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +124 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +108 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +121 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +114 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +27 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts +30 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/scoreRailAlignment.ts +67 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/types.ts +34 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -60
- package/lib/utils/doesPathCoincideWithTraces.ts +61 -0
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
- package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
- package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -1
- package/tests/examples/__snapshots__/example02.snap.svg +1 -1
- package/tests/examples/__snapshots__/example09.snap.svg +9 -13
- package/tests/examples/__snapshots__/example14.snap.svg +1 -1
- package/tests/examples/__snapshots__/example18.snap.svg +1 -1
- package/tests/examples/__snapshots__/example21.snap.svg +30 -32
- package/tests/examples/__snapshots__/example29.snap.svg +13 -19
- package/tests/examples/__snapshots__/example46.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
- package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
- package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
- package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +24 -0
- package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
- package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +38 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-component-scope.test.ts +38 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-eligible-traces.test.ts +28 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-horizontal.test.ts +62 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-anchor.test.ts +36 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-junction.test.ts +42 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-obstacle.test.ts +28 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +52 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-vertical.test.ts +25 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-visible-length.test.ts +40 -0
- 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
|
+
}
|