@tscircuit/schematic-trace-solver 0.0.96 → 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 +986 -479
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +55 -14
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +31 -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 +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 +178 -0
- package/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg +54 -0
- package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
- package/tests/repros/assets/repro-rp2040-gamepad-trace-alignment.input.json +237 -0
- package/tests/repros/assets/repro-rp2040-zero-crystal-fallback-netlabels.input.json +121 -0
- package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +38 -0
- package/tests/repros/repro-rp2040-zero-crystal-fallback-netlabels.test.ts +16 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/segment-intersects-rect-interior.test.ts +15 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/segment-overlaps-rect-boundary-boundary.test.ts +18 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/segment-overlaps-rect-boundary-interior.test.ts +18 -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
|
@@ -1,24 +1,29 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { calculateElbow } from "calculate-elbow"
|
|
1
3
|
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
-
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
3
4
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
5
|
import type { MspConnectionPair } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
7
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
8
|
+
import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
9
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
10
|
+
import type { RectPadding } from "lib/utils/textBoxBounds"
|
|
8
11
|
import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
|
|
9
|
-
import {
|
|
10
|
-
import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
|
|
12
|
+
import { calculateDirectShortPath } from "./calculateDirectShortPath"
|
|
11
13
|
import {
|
|
14
|
+
findFirstCollision,
|
|
15
|
+
isHorizontal,
|
|
16
|
+
isVertical,
|
|
17
|
+
segmentOverlapsRectBoundary,
|
|
18
|
+
} from "./collisions"
|
|
19
|
+
import {
|
|
20
|
+
type Axis,
|
|
12
21
|
aabbFromPoints,
|
|
13
22
|
candidateMidsFromSet,
|
|
14
23
|
midBetweenPointAndRect,
|
|
15
|
-
type Axis,
|
|
16
24
|
} from "./mid"
|
|
17
25
|
import { pathKey, shiftSegmentOrth } from "./pathOps"
|
|
18
|
-
import type
|
|
19
|
-
import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
20
|
-
import type { RectPadding } from "lib/utils/textBoxBounds"
|
|
21
|
-
import { calculateDirectShortPath } from "./calculateDirectShortPath"
|
|
26
|
+
import { getObstacleRects, type ObstacleRect } from "./rect"
|
|
22
27
|
|
|
23
28
|
type PathKey = string
|
|
24
29
|
|
|
@@ -268,9 +273,42 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
268
273
|
const collision = findFirstCollision(path, this.obstacles, {
|
|
269
274
|
excludeRectsForSegment: (segIndex) => {
|
|
270
275
|
const lastSegIndex = path.length - 2
|
|
276
|
+
const excludedRects = new Set<ObstacleRect>()
|
|
277
|
+
|
|
271
278
|
if (segIndex === 0 || segIndex === lastSegIndex) {
|
|
272
|
-
|
|
279
|
+
for (const textObstacle of this.textObstacles) {
|
|
280
|
+
excludedRects.add(textObstacle)
|
|
281
|
+
}
|
|
273
282
|
}
|
|
283
|
+
|
|
284
|
+
const segmentStart = path[segIndex]!
|
|
285
|
+
const segmentEnd = path[segIndex + 1]!
|
|
286
|
+
const endpointEpsilon = 1e-9
|
|
287
|
+
const segmentTouchesPin = (pin: MspConnectionPair["pins"][number]) =>
|
|
288
|
+
[segmentStart, segmentEnd].some(
|
|
289
|
+
(point) =>
|
|
290
|
+
Math.abs(point.x - pin.x) <= endpointEpsilon &&
|
|
291
|
+
Math.abs(point.y - pin.y) <= endpointEpsilon,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
for (const pin of this.pins) {
|
|
295
|
+
if (!segmentTouchesPin(pin)) continue
|
|
296
|
+
const endpointChipObstacle = this.obstacles.find(
|
|
297
|
+
(obstacle) =>
|
|
298
|
+
obstacle.kind === "chip" && obstacle.chipId === pin.chipId,
|
|
299
|
+
)
|
|
300
|
+
if (
|
|
301
|
+
endpointChipObstacle &&
|
|
302
|
+
segmentOverlapsRectBoundary(
|
|
303
|
+
segmentStart,
|
|
304
|
+
segmentEnd,
|
|
305
|
+
endpointChipObstacle,
|
|
306
|
+
)
|
|
307
|
+
) {
|
|
308
|
+
excludedRects.add(endpointChipObstacle)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
274
312
|
const adjacentEndpointSegIndex =
|
|
275
313
|
segIndex === 1
|
|
276
314
|
? 2
|
|
@@ -286,9 +324,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
286
324
|
path[adjacentEndpointSegIndex + 1]!,
|
|
287
325
|
)
|
|
288
326
|
) {
|
|
289
|
-
|
|
327
|
+
for (const textObstacle of this.endpointTextObstacles) {
|
|
328
|
+
excludedRects.add(textObstacle)
|
|
329
|
+
}
|
|
290
330
|
}
|
|
291
|
-
|
|
331
|
+
|
|
332
|
+
return excludedRects
|
|
292
333
|
},
|
|
293
334
|
})
|
|
294
335
|
|
|
@@ -35,6 +35,37 @@ export const segmentIntersectsRect = <TRect extends RectBounds>(
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export const segmentOverlapsRectBoundary = <TRect extends RectBounds>(
|
|
39
|
+
a: Point,
|
|
40
|
+
b: Point,
|
|
41
|
+
r: TRect,
|
|
42
|
+
eps = EPS,
|
|
43
|
+
): boolean => {
|
|
44
|
+
if (isVertical(a, b, eps)) {
|
|
45
|
+
const onVerticalBoundary =
|
|
46
|
+
Math.abs(a.x - r.minX) <= eps || Math.abs(a.x - r.maxX) <= eps
|
|
47
|
+
if (!onVerticalBoundary) return false
|
|
48
|
+
|
|
49
|
+
const overlap =
|
|
50
|
+
Math.min(Math.max(a.y, b.y), r.maxY) -
|
|
51
|
+
Math.max(Math.min(a.y, b.y), r.minY)
|
|
52
|
+
return overlap > eps
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isHorizontal(a, b, eps)) {
|
|
56
|
+
const onHorizontalBoundary =
|
|
57
|
+
Math.abs(a.y - r.minY) <= eps || Math.abs(a.y - r.maxY) <= eps
|
|
58
|
+
if (!onHorizontalBoundary) return false
|
|
59
|
+
|
|
60
|
+
const overlap =
|
|
61
|
+
Math.min(Math.max(a.x, b.x), r.maxX) -
|
|
62
|
+
Math.max(Math.min(a.x, b.x), r.minX)
|
|
63
|
+
return overlap > eps
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
export const findFirstCollision = <TRect extends RectBounds>(
|
|
39
70
|
pts: Point[],
|
|
40
71
|
rects: TRect[],
|
|
@@ -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
|
+
}
|