@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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { distance } from "@tscircuit/math-utils"
|
|
2
|
+
import type { ConnectivityMap } from "connectivity-map"
|
|
3
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
1
4
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
2
5
|
import type {
|
|
3
6
|
InputChip,
|
|
@@ -5,17 +8,17 @@ import type {
|
|
|
5
8
|
InputProblem,
|
|
6
9
|
PinId,
|
|
7
10
|
} from "lib/types/InputProblem"
|
|
8
|
-
import { ConnectivityMap } from "connectivity-map"
|
|
9
|
-
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
10
|
-
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
11
|
-
import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
|
|
12
|
-
import type { GraphicsObject } from "graphics-debug"
|
|
13
11
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
14
|
-
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
15
12
|
import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDifferentSchematicSections"
|
|
13
|
+
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
14
|
+
import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
|
|
15
|
+
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
16
|
+
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
16
17
|
|
|
17
18
|
export type MspConnectionPairId = string
|
|
18
19
|
|
|
20
|
+
const getPinPairKey = (pinIds: [PinId, PinId]) => [...pinIds].sort().join("::")
|
|
21
|
+
|
|
19
22
|
export type MspConnectionPair = {
|
|
20
23
|
mspPairId: MspConnectionPairId
|
|
21
24
|
dcConnNetId: string
|
|
@@ -36,6 +39,7 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
36
39
|
|
|
37
40
|
pinMap: Record<string, InputPin & { chipId: string }>
|
|
38
41
|
userNetIdByPinId: Record<string, string | undefined>
|
|
42
|
+
directConnectionPinPairKeys: Set<string>
|
|
39
43
|
|
|
40
44
|
constructor({ inputProblem }: { inputProblem: InputProblem }) {
|
|
41
45
|
super()
|
|
@@ -62,7 +66,9 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
62
66
|
|
|
63
67
|
// Build a mapping from PinId to user-provided netId (if any)
|
|
64
68
|
this.userNetIdByPinId = {}
|
|
69
|
+
this.directConnectionPinPairKeys = new Set()
|
|
65
70
|
for (const dc of inputProblem.directConnections) {
|
|
71
|
+
this.directConnectionPinPairKeys.add(getPinPairKey(dc.pinIds))
|
|
66
72
|
if (dc.netId) {
|
|
67
73
|
const [a, b] = dc.pinIds
|
|
68
74
|
this.userNetIdByPinId[a] = dc.netId
|
|
@@ -105,9 +111,13 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
105
111
|
const [pin1, pin2] = directlyConnectedPins
|
|
106
112
|
const p1 = this.pinMap[pin1!]!
|
|
107
113
|
const p2 = this.pinMap[pin2!]!
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
const pinPairKey = getPinPairKey([pin1!, pin2!])
|
|
115
|
+
// Explicit source traces are classified by straight-line distance when
|
|
116
|
+
// their input is created; named nets retain the orthogonal route metric.
|
|
117
|
+
const pairDistance = this.directConnectionPinPairKeys.has(pinPairKey)
|
|
118
|
+
? distance(p1, p2)
|
|
119
|
+
: Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
|
|
120
|
+
if (pairDistance > this.maxMspPairDistance) {
|
|
111
121
|
// Too far apart; skip creating an MSP pair for this net
|
|
112
122
|
return
|
|
113
123
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isVertical,
|
|
17
17
|
segmentOverlapsRectBoundary,
|
|
18
18
|
} from "./collisions"
|
|
19
|
+
import { generateEndpointCollisionDetours } from "./generateEndpointCollisionDetours"
|
|
19
20
|
import {
|
|
20
21
|
type Axis,
|
|
21
22
|
aabbFromPoints,
|
|
@@ -357,6 +358,35 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
357
358
|
const isFirstSegment = segIndex === 0
|
|
358
359
|
const isLastSegment = segIndex === path.length - 2
|
|
359
360
|
|
|
361
|
+
if (path.length === 3 && (isFirstSegment || isLastSegment)) {
|
|
362
|
+
const detours = generateEndpointCollisionDetours({
|
|
363
|
+
path,
|
|
364
|
+
collidingSegmentIndex: segIndex,
|
|
365
|
+
obstacle: rect,
|
|
366
|
+
})
|
|
367
|
+
.filter((detour) => {
|
|
368
|
+
const key = pathKey(detour)
|
|
369
|
+
if (this.visited.has(key)) return false
|
|
370
|
+
this.visited.add(key)
|
|
371
|
+
return true
|
|
372
|
+
})
|
|
373
|
+
.sort(
|
|
374
|
+
(a, b) =>
|
|
375
|
+
this.pathLength(a) - this.pathLength(b) ||
|
|
376
|
+
this.getPinBandPenalty(a) - this.getPinBandPenalty(b),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
for (const detour of detours) {
|
|
380
|
+
const nextCollisionRects = new Set(collisionRects)
|
|
381
|
+
nextCollisionRects.add(rect)
|
|
382
|
+
this.queue.push({
|
|
383
|
+
path: detour,
|
|
384
|
+
collisionRects: nextCollisionRects,
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
360
390
|
if (isFirstSegment) {
|
|
361
391
|
// If first segment collides, move the second segment instead
|
|
362
392
|
if (path.length < 3) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { isHorizontal, isVertical } from "./collisions"
|
|
3
|
+
import { type Axis, midBetweenPointAndRect } from "./mid"
|
|
4
|
+
import type { ObstacleRect } from "./rect"
|
|
5
|
+
|
|
6
|
+
const getSegmentAxis = (start: Point, end: Point): Axis | null => {
|
|
7
|
+
if (isVertical(start, end)) return "x"
|
|
8
|
+
if (isHorizontal(start, end)) return "y"
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const hasOnlyNonzeroOrthogonalSegments = (path: Point[]) =>
|
|
13
|
+
path.every((point, index) => {
|
|
14
|
+
const nextPoint = path[index + 1]
|
|
15
|
+
if (!nextPoint) return true
|
|
16
|
+
if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const generateEndpointCollisionDetours = ({
|
|
23
|
+
path,
|
|
24
|
+
collidingSegmentIndex,
|
|
25
|
+
obstacle,
|
|
26
|
+
}: {
|
|
27
|
+
path: Point[]
|
|
28
|
+
collidingSegmentIndex: number
|
|
29
|
+
obstacle: ObstacleRect
|
|
30
|
+
}): Point[][] => {
|
|
31
|
+
if (path.length !== 3) return []
|
|
32
|
+
|
|
33
|
+
const lastSegmentIndex = path.length - 2
|
|
34
|
+
if (
|
|
35
|
+
collidingSegmentIndex !== 0 &&
|
|
36
|
+
collidingSegmentIndex !== lastSegmentIndex
|
|
37
|
+
) {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const shouldReverse = collidingSegmentIndex === lastSegmentIndex
|
|
42
|
+
const orderedPath = shouldReverse ? [...path].reverse() : path
|
|
43
|
+
const [start, corner, end] = orderedPath
|
|
44
|
+
const firstSegmentAxis = getSegmentAxis(start!, corner!)
|
|
45
|
+
const secondSegmentAxis = getSegmentAxis(corner!, end!)
|
|
46
|
+
if (!firstSegmentAxis || !secondSegmentAxis) return []
|
|
47
|
+
if (firstSegmentAxis === secondSegmentAxis) return []
|
|
48
|
+
|
|
49
|
+
const escapeCoordinates = [
|
|
50
|
+
...midBetweenPointAndRect(secondSegmentAxis, start!, obstacle),
|
|
51
|
+
...midBetweenPointAndRect(secondSegmentAxis, end!, obstacle),
|
|
52
|
+
]
|
|
53
|
+
const detourCoordinates = [
|
|
54
|
+
...midBetweenPointAndRect(firstSegmentAxis, start!, obstacle),
|
|
55
|
+
...midBetweenPointAndRect(firstSegmentAxis, end!, obstacle),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const detours: Point[][] = []
|
|
59
|
+
for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
|
|
60
|
+
for (const detourCoordinate of [...new Set(detourCoordinates)]) {
|
|
61
|
+
const orderedDetour =
|
|
62
|
+
firstSegmentAxis === "y"
|
|
63
|
+
? [
|
|
64
|
+
start!,
|
|
65
|
+
{ x: escapeCoordinate, y: start!.y },
|
|
66
|
+
{ x: escapeCoordinate, y: detourCoordinate },
|
|
67
|
+
{ x: end!.x, y: detourCoordinate },
|
|
68
|
+
end!,
|
|
69
|
+
]
|
|
70
|
+
: [
|
|
71
|
+
start!,
|
|
72
|
+
{ x: start!.x, y: escapeCoordinate },
|
|
73
|
+
{ x: detourCoordinate, y: escapeCoordinate },
|
|
74
|
+
{ x: detourCoordinate, y: end!.y },
|
|
75
|
+
end!,
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour
|
|
79
|
+
if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return detours
|
|
84
|
+
}
|
|
@@ -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
|
+
}
|