@tscircuit/schematic-trace-solver 0.0.97 → 0.0.98
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 +9 -0
- package/dist/index.js +753 -285
- 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 +1 -1
- 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__/example14.snap.svg +1 -1
- package/tests/examples/__snapshots__/example18.snap.svg +1 -1
- 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__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
- package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +24 -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
|
@@ -79,7 +79,9 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
79
79
|
availableNetOrientationSolver?: AvailableNetOrientationSolver
|
|
80
80
|
railNetLabelCornerPlacementSolver?: RailNetLabelCornerPlacementSolver
|
|
81
81
|
traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
|
|
82
|
+
preAlignmentNetLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
|
|
82
83
|
netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
|
|
84
|
+
traceCleanupSolver2?: TraceCleanupSolver
|
|
83
85
|
netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
|
|
84
86
|
|
|
85
87
|
startTimeOfPhase: Record<string, number>
|
|
@@ -291,7 +293,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
291
293
|
],
|
|
292
294
|
),
|
|
293
295
|
definePipelineStep(
|
|
294
|
-
"
|
|
296
|
+
"preAlignmentNetLabelTraceCollisionSolver",
|
|
295
297
|
NetLabelTraceCollisionSolver,
|
|
296
298
|
(instance) => [
|
|
297
299
|
{
|
|
@@ -303,6 +305,48 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
303
305
|
},
|
|
304
306
|
],
|
|
305
307
|
),
|
|
308
|
+
definePipelineStep(
|
|
309
|
+
"traceCleanupSolver2",
|
|
310
|
+
TraceCleanupSolver,
|
|
311
|
+
(instance) => {
|
|
312
|
+
const collisionOutput =
|
|
313
|
+
instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
314
|
+
const labelMergingOutput =
|
|
315
|
+
instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
|
|
316
|
+
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
inputProblem: instance.inputProblem,
|
|
320
|
+
allTraces: collisionOutput.traces,
|
|
321
|
+
allLabelPlacements: collisionOutput.netLabelPlacements,
|
|
322
|
+
mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
|
|
323
|
+
paddingBuffer: 0.1,
|
|
324
|
+
operations: ["aligning_same_net_rails"],
|
|
325
|
+
eligibleTraceIds: new Set(
|
|
326
|
+
instance
|
|
327
|
+
.traceCleanupSolver!.getOutput()
|
|
328
|
+
.traces.map((trace) => trace.mspPairId),
|
|
329
|
+
),
|
|
330
|
+
},
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
),
|
|
334
|
+
definePipelineStep(
|
|
335
|
+
"netLabelTraceCollisionSolver",
|
|
336
|
+
NetLabelTraceCollisionSolver,
|
|
337
|
+
(instance) => {
|
|
338
|
+
const previousCollisionOutput =
|
|
339
|
+
instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
340
|
+
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
inputProblem: instance.inputProblem,
|
|
344
|
+
traces: instance.traceCleanupSolver2!.getOutput().traces,
|
|
345
|
+
netLabelPlacements: previousCollisionOutput.netLabelPlacements,
|
|
346
|
+
},
|
|
347
|
+
]
|
|
348
|
+
},
|
|
349
|
+
),
|
|
306
350
|
definePipelineStep(
|
|
307
351
|
"netLabelNetLabelCollisionSolver",
|
|
308
352
|
NetLabelNetLabelCollisionSolver,
|
|
@@ -6,16 +6,25 @@ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
|
6
6
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
7
7
|
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
8
8
|
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
9
|
+
import { alignSameNetRails } from "./alignSameNetRails"
|
|
10
|
+
|
|
11
|
+
export type TraceCleanupOperation =
|
|
12
|
+
| "untangling_traces"
|
|
13
|
+
| "minimizing_turns"
|
|
14
|
+
| "balancing_l_shapes"
|
|
15
|
+
| "aligning_same_net_rails"
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Defines the input structure for the TraceCleanupSolver.
|
|
12
19
|
*/
|
|
13
|
-
interface TraceCleanupSolverInput {
|
|
20
|
+
export interface TraceCleanupSolverInput {
|
|
14
21
|
inputProblem: InputProblem
|
|
15
22
|
allTraces: SolvedTracePath[]
|
|
16
23
|
allLabelPlacements: NetLabelPlacement[]
|
|
17
24
|
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
18
25
|
paddingBuffer: number
|
|
26
|
+
operations?: readonly TraceCleanupOperation[]
|
|
27
|
+
eligibleTraceIds?: ReadonlySet<string>
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
|
|
@@ -24,10 +33,11 @@ import { is4PointRectangle } from "./is4PointRectangle"
|
|
|
24
33
|
/**
|
|
25
34
|
* Represents the different stages or steps within the trace cleanup pipeline.
|
|
26
35
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const DEFAULT_OPERATIONS: readonly TraceCleanupOperation[] = [
|
|
37
|
+
"untangling_traces",
|
|
38
|
+
"minimizing_turns",
|
|
39
|
+
"balancing_l_shapes",
|
|
40
|
+
]
|
|
31
41
|
|
|
32
42
|
/**
|
|
33
43
|
* The TraceCleanupSolver is responsible for improving the aesthetics and readability of schematic traces.
|
|
@@ -42,7 +52,9 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
42
52
|
private outputTraces: SolvedTracePath[]
|
|
43
53
|
private traceIdQueue: string[]
|
|
44
54
|
private tracesMap: Map<string, SolvedTracePath>
|
|
45
|
-
private
|
|
55
|
+
private operations: readonly TraceCleanupOperation[]
|
|
56
|
+
private operationIndex = 0
|
|
57
|
+
private pipelineStep: TraceCleanupOperation | null
|
|
46
58
|
private activeTraceId: string | null = null // New property
|
|
47
59
|
override activeSubSolver: BaseSolver | null = null
|
|
48
60
|
|
|
@@ -51,6 +63,8 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
51
63
|
this.input = solverInput
|
|
52
64
|
this.outputTraces = [...solverInput.allTraces]
|
|
53
65
|
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
|
|
66
|
+
this.operations = solverInput.operations ?? DEFAULT_OPERATIONS
|
|
67
|
+
this.pipelineStep = this.operations[0] ?? null
|
|
54
68
|
this.traceIdQueue = Array.from(
|
|
55
69
|
solverInput.allTraces.map((e) => e.mspPairId),
|
|
56
70
|
)
|
|
@@ -66,14 +80,19 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
66
80
|
this.outputTraces = output.traces
|
|
67
81
|
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
|
|
68
82
|
this.activeSubSolver = null
|
|
69
|
-
this.
|
|
83
|
+
this._advancePipeline()
|
|
70
84
|
} else if (this.activeSubSolver.failed) {
|
|
71
85
|
this.activeSubSolver = null
|
|
72
|
-
this.
|
|
86
|
+
this._advancePipeline()
|
|
73
87
|
}
|
|
74
88
|
return
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
if (!this.pipelineStep) {
|
|
92
|
+
this.solved = true
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
switch (this.pipelineStep) {
|
|
78
97
|
case "untangling_traces":
|
|
79
98
|
this._runUntangleTracesStep()
|
|
@@ -84,9 +103,19 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
84
103
|
case "balancing_l_shapes":
|
|
85
104
|
this._runBalanceLShapesStep()
|
|
86
105
|
break
|
|
106
|
+
case "aligning_same_net_rails":
|
|
107
|
+
this._runAlignSameNetRailsStep()
|
|
108
|
+
break
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
|
|
112
|
+
private _advancePipeline() {
|
|
113
|
+
this.operationIndex++
|
|
114
|
+
this.pipelineStep = this.operations[this.operationIndex] ?? null
|
|
115
|
+
this.traceIdQueue = this.outputTraces.map((trace) => trace.mspPairId)
|
|
116
|
+
if (this.pipelineStep) this.activeTraceId = null
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
private _runUntangleTracesStep() {
|
|
91
120
|
this.activeSubSolver = new UntangleTraceSubsolver({
|
|
92
121
|
...this.input,
|
|
@@ -96,10 +125,7 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
96
125
|
|
|
97
126
|
private _runMinimizeTurnsStep() {
|
|
98
127
|
if (this.traceIdQueue.length === 0) {
|
|
99
|
-
this.
|
|
100
|
-
this.traceIdQueue = Array.from(
|
|
101
|
-
this.input.allTraces.map((e) => e.mspPairId),
|
|
102
|
-
)
|
|
128
|
+
this._advancePipeline()
|
|
103
129
|
return
|
|
104
130
|
}
|
|
105
131
|
|
|
@@ -108,7 +134,7 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
108
134
|
|
|
109
135
|
private _runBalanceLShapesStep() {
|
|
110
136
|
if (this.traceIdQueue.length === 0) {
|
|
111
|
-
this.
|
|
137
|
+
this._advancePipeline()
|
|
112
138
|
return
|
|
113
139
|
}
|
|
114
140
|
|
|
@@ -146,6 +172,22 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
146
172
|
this.outputTraces = Array.from(this.tracesMap.values())
|
|
147
173
|
}
|
|
148
174
|
|
|
175
|
+
private _runAlignSameNetRailsStep() {
|
|
176
|
+
const alignment = alignSameNetRails({
|
|
177
|
+
inputProblem: this.input.inputProblem,
|
|
178
|
+
traces: this.outputTraces,
|
|
179
|
+
netLabelPlacements: this.input.allLabelPlacements,
|
|
180
|
+
eligibleTraceIds:
|
|
181
|
+
this.input.eligibleTraceIds ??
|
|
182
|
+
new Set(this.outputTraces.map((trace) => trace.mspPairId)),
|
|
183
|
+
})
|
|
184
|
+
this.outputTraces = alignment.traces
|
|
185
|
+
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
|
|
186
|
+
this.stats.alignedRailGroupCount = alignment.alignedRailGroupCount
|
|
187
|
+
this.stats.alignedTraceCount = alignment.alignedTraceCount
|
|
188
|
+
this._advancePipeline()
|
|
189
|
+
}
|
|
190
|
+
|
|
149
191
|
getOutput() {
|
|
150
192
|
return {
|
|
151
193
|
traces: this.outputTraces,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
2
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
4
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
5
|
+
import { evaluateRailGroup } from "./sameNetRailAlignment/evaluateRailGroup"
|
|
6
|
+
import { getRailGroups } from "./sameNetRailAlignment/getRailGroups"
|
|
7
|
+
import type { AlignmentCandidate } from "./sameNetRailAlignment/types"
|
|
8
|
+
|
|
9
|
+
interface AlignSameNetRailsInput {
|
|
10
|
+
inputProblem: InputProblem
|
|
11
|
+
traces: SolvedTracePath[]
|
|
12
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
13
|
+
eligibleTraceIds: ReadonlySet<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const alignSameNetRails = ({
|
|
17
|
+
inputProblem,
|
|
18
|
+
traces,
|
|
19
|
+
netLabelPlacements,
|
|
20
|
+
eligibleTraceIds,
|
|
21
|
+
}: AlignSameNetRailsInput): {
|
|
22
|
+
traces: SolvedTracePath[]
|
|
23
|
+
alignedRailGroupCount: number
|
|
24
|
+
alignedTraceCount: number
|
|
25
|
+
} => {
|
|
26
|
+
let outputTraces = [...traces]
|
|
27
|
+
const obstacles = getObstacleRects(inputProblem)
|
|
28
|
+
const alignedTraceIds = new Set<string>()
|
|
29
|
+
let alignedRailGroupCount = 0
|
|
30
|
+
const maximumPasses = Math.max(
|
|
31
|
+
1,
|
|
32
|
+
traces.reduce((sum, trace) => sum + trace.tracePath.length, 0),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for (let pass = 0; pass < maximumPasses; pass++) {
|
|
36
|
+
const groups = getRailGroups(
|
|
37
|
+
outputTraces,
|
|
38
|
+
eligibleTraceIds,
|
|
39
|
+
inputProblem,
|
|
40
|
+
obstacles,
|
|
41
|
+
)
|
|
42
|
+
let applied: AlignmentCandidate | null = null
|
|
43
|
+
|
|
44
|
+
for (const group of groups) {
|
|
45
|
+
applied = evaluateRailGroup({
|
|
46
|
+
group,
|
|
47
|
+
traces: outputTraces,
|
|
48
|
+
netLabelPlacements,
|
|
49
|
+
obstacles,
|
|
50
|
+
eligibleTraceIds,
|
|
51
|
+
})
|
|
52
|
+
if (applied) break
|
|
53
|
+
}
|
|
54
|
+
if (!applied) break
|
|
55
|
+
|
|
56
|
+
outputTraces = applied.traces
|
|
57
|
+
alignedRailGroupCount++
|
|
58
|
+
for (const traceId of applied.changedTraceIds) alignedTraceIds.add(traceId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
traces: outputTraces,
|
|
63
|
+
alignedRailGroupCount,
|
|
64
|
+
alignedTraceCount: alignedTraceIds.size,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
2
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
4
|
+
import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
|
+
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
6
|
+
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
|
|
7
|
+
import { getDistinctCoordinates, pointsEqual } from "./geometry"
|
|
8
|
+
import { moveRailSegments } from "./moveRailSegments"
|
|
9
|
+
import { preservesLabelAnchors } from "./preservesLabelAnchors"
|
|
10
|
+
import {
|
|
11
|
+
getTraceGeometryMetrics,
|
|
12
|
+
isReadabilityImprovement,
|
|
13
|
+
scoreIsBetter,
|
|
14
|
+
} from "./scoreRailAlignment"
|
|
15
|
+
import type { AlignmentCandidate, AlignmentScore, RailSegment } from "./types"
|
|
16
|
+
|
|
17
|
+
interface EvaluateRailGroupInput {
|
|
18
|
+
group: RailSegment[]
|
|
19
|
+
traces: SolvedTracePath[]
|
|
20
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
21
|
+
obstacles: ObstacleRect[]
|
|
22
|
+
eligibleTraceIds: ReadonlySet<string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tracePathChanged = (
|
|
26
|
+
original: SolvedTracePath,
|
|
27
|
+
candidate: SolvedTracePath,
|
|
28
|
+
) =>
|
|
29
|
+
original.tracePath.length !== candidate.tracePath.length ||
|
|
30
|
+
candidate.tracePath.some(
|
|
31
|
+
(point, index) => !pointsEqual(point, original.tracePath[index]!),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export const evaluateRailGroup = ({
|
|
35
|
+
group,
|
|
36
|
+
traces,
|
|
37
|
+
netLabelPlacements,
|
|
38
|
+
obstacles,
|
|
39
|
+
eligibleTraceIds,
|
|
40
|
+
}: EvaluateRailGroupInput): AlignmentCandidate | null => {
|
|
41
|
+
const groupTraceIds = new Set(group.map((segment) => segment.traceId))
|
|
42
|
+
const originalGroupTraces = traces.filter((trace) =>
|
|
43
|
+
groupTraceIds.has(trace.mspPairId),
|
|
44
|
+
)
|
|
45
|
+
const baseline = getTraceGeometryMetrics(originalGroupTraces, traces)
|
|
46
|
+
const coordinates = getDistinctCoordinates(
|
|
47
|
+
group.map((segment) => segment.coordinate),
|
|
48
|
+
)
|
|
49
|
+
const otherNetTraces = traces.filter(
|
|
50
|
+
(trace) => trace.globalConnNetId !== group[0]!.globalConnNetId,
|
|
51
|
+
)
|
|
52
|
+
const immutableSameNetTraces = traces.filter(
|
|
53
|
+
(trace) =>
|
|
54
|
+
trace.globalConnNetId === group[0]!.globalConnNetId &&
|
|
55
|
+
!eligibleTraceIds.has(trace.mspPairId),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
let best: AlignmentCandidate | null = null
|
|
59
|
+
for (const coordinate of coordinates) {
|
|
60
|
+
const candidateMap = new Map<string, SolvedTracePath>()
|
|
61
|
+
|
|
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
|
+
}
|
|
70
|
+
|
|
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,
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
if (!candidatesAreClear) continue
|
|
91
|
+
if (
|
|
92
|
+
!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)
|
|
93
|
+
) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metrics = getTraceGeometryMetrics(candidateTraces, allCandidateTraces)
|
|
98
|
+
if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
|
|
99
|
+
if (!isReadabilityImprovement(metrics, baseline)) continue
|
|
100
|
+
|
|
101
|
+
const score: AlignmentScore = {
|
|
102
|
+
...metrics,
|
|
103
|
+
displacement: group.reduce(
|
|
104
|
+
(sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
|
|
105
|
+
0,
|
|
106
|
+
),
|
|
107
|
+
coordinate,
|
|
108
|
+
}
|
|
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
|
+
|
|
119
|
+
const candidate = { traces: allCandidateTraces, changedTraceIds, score }
|
|
120
|
+
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return best
|
|
124
|
+
}
|
|
@@ -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
|
+
}
|