@tscircuit/schematic-trace-solver 0.0.124 → 0.0.126
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 +46 -1
- package/dist/index.js +461 -37
- package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +5 -1
- package/lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +15 -0
- package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +49 -6
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +69 -11
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +216 -0
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/generateElbowTransitionSimplificationCandidates.ts +186 -0
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/types.ts +10 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +3 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +16 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +56 -13
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260805T061316Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260806T061548Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260806T093501Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +6 -6
- package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +212 -0
- package/tests/bug-reports/bug-report-20260805T061316Z/bug-report-20260805T061316Z.json +889 -0
- package/tests/bug-reports/bug-report-20260805T061316Z/bug-report-20260805T061316Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +212 -0
- package/tests/bug-reports/bug-report-20260806T061548Z/bug-report-20260806T061548Z.json +960 -0
- package/tests/bug-reports/bug-report-20260806T061548Z/bug-report-20260806T061548Z.test.ts +32 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +135 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +505 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.test.ts +66 -0
- package/tests/examples/__snapshots__/example01.snap.svg +18 -18
- package/tests/examples/__snapshots__/example33.snap.svg +1 -1
|
@@ -41,7 +41,11 @@ export const traceCrossesBoundsInterior = (
|
|
|
41
41
|
return false
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const segmentCrossesBoundsInterior = (
|
|
44
|
+
export const segmentCrossesBoundsInterior = (
|
|
45
|
+
p1: Point,
|
|
46
|
+
p2: Point,
|
|
47
|
+
bounds: Bounds,
|
|
48
|
+
) => {
|
|
45
49
|
const interiorBounds = {
|
|
46
50
|
minX: bounds.minX + TRACE_BOUNDARY_TOLERANCE,
|
|
47
51
|
minY: bounds.minY + TRACE_BOUNDARY_TOLERANCE,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
|
|
13
13
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
14
14
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
15
|
+
import type { CompletedTraceReroute } from "lib/solvers/TraceElbowTransitionSimplificationSolver/types"
|
|
15
16
|
|
|
16
17
|
const ON_PATH_EPS = 1e-6
|
|
17
18
|
|
|
@@ -172,6 +173,7 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
172
173
|
|
|
173
174
|
outputTraces: SolvedTracePath[]
|
|
174
175
|
outputNetLabelPlacements: NetLabelPlacement[]
|
|
176
|
+
completedReroutes: CompletedTraceReroute[] = []
|
|
175
177
|
|
|
176
178
|
override activeSubSolver: SingleOverlapSolver | null = null
|
|
177
179
|
private recentlyFailed = new Set<string>()
|
|
@@ -208,6 +210,17 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
208
210
|
if (this.activeSubSolver.solved) {
|
|
209
211
|
const solvedPath = this.activeSubSolver.solvedTracePath
|
|
210
212
|
if (solvedPath) {
|
|
213
|
+
this.completedReroutes.push({
|
|
214
|
+
initialTrace: {
|
|
215
|
+
...this.activeSubSolver.initialTrace,
|
|
216
|
+
tracePath: this.activeSubSolver.initialTrace.tracePath.map(
|
|
217
|
+
(point) => ({ ...point }),
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
reroutedTracePath: solvedPath.map((point) => ({ ...point })),
|
|
221
|
+
label: this.activeSubSolver.label,
|
|
222
|
+
detourCount: this.activeSubSolver.detourCount,
|
|
223
|
+
})
|
|
211
224
|
const idx = this.outputTraces.findIndex(
|
|
212
225
|
(t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
|
|
213
226
|
)
|
|
@@ -282,6 +295,7 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
282
295
|
paddingBuffer: PADDING_BUFFER,
|
|
283
296
|
detourCount,
|
|
284
297
|
tracesToAvoidOverlapping: this.outputTraces,
|
|
298
|
+
netLabelPlacements: this.outputNetLabelPlacements,
|
|
285
299
|
})
|
|
286
300
|
}
|
|
287
301
|
|
|
@@ -319,6 +333,7 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
|
|
|
319
333
|
getOutput() {
|
|
320
334
|
return {
|
|
321
335
|
traces: this.outputTraces,
|
|
336
|
+
completedReroutes: this.completedReroutes,
|
|
322
337
|
netLabelPlacements: this.outputNetLabelPlacements,
|
|
323
338
|
}
|
|
324
339
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { GraphicsObject } from "graphics-debug"
|
|
2
2
|
import type { Point } from "@tscircuit/math-utils"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
segmentCrossesBoundsInterior,
|
|
5
|
+
traceCrossesBoundsInterior,
|
|
6
|
+
} from "lib/solvers/AvailableNetOrientationSolver/geometry"
|
|
4
7
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
5
8
|
import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
|
|
6
9
|
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
@@ -32,6 +35,8 @@ import type {
|
|
|
32
35
|
import { visualizeRailNetLabelCornerPlacementSolver } from "./visualize"
|
|
33
36
|
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
34
37
|
|
|
38
|
+
const LABEL_TRACE_CLEARANCE = 0.1
|
|
39
|
+
|
|
35
40
|
export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
36
41
|
inputProblem: InputProblem
|
|
37
42
|
traces: SolvedTracePath[]
|
|
@@ -271,11 +276,12 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
271
276
|
const labelTraces = this.getTraceLinesForLabel(label)
|
|
272
277
|
const allowRailAlignedFallback =
|
|
273
278
|
this.isConfiguredRailLabel(label) &&
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
(this.isLabelCrossedByOtherNetTrace(label) ||
|
|
280
|
+
!labelTraces.some((trace) =>
|
|
281
|
+
getTraceCorners(trace.tracePath).some((corner) =>
|
|
282
|
+
this.pointsEqual(corner, label.anchorPoint),
|
|
283
|
+
),
|
|
284
|
+
))
|
|
279
285
|
|
|
280
286
|
for (const trace of labelTraces) {
|
|
281
287
|
const path = trace.tracePath
|
|
@@ -324,6 +330,17 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
324
330
|
)
|
|
325
331
|
}
|
|
326
332
|
|
|
333
|
+
private isLabelCrossedByOtherNetTrace(label: NetLabelPlacement) {
|
|
334
|
+
const bounds = getRectBounds(label.center, label.width, label.height)
|
|
335
|
+
const otherNetTraceMap = Object.fromEntries(
|
|
336
|
+
this.traces
|
|
337
|
+
.filter((trace) => trace.globalConnNetId !== label.globalConnNetId)
|
|
338
|
+
.map((trace) => [trace.mspPairId, trace]),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return traceCrossesBoundsInterior(bounds, otherNetTraceMap)
|
|
342
|
+
}
|
|
343
|
+
|
|
327
344
|
private pointsEqual(a: Point, b: Point) {
|
|
328
345
|
return Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS
|
|
329
346
|
}
|
|
@@ -394,6 +411,32 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
394
411
|
shiftedCoordinates.add(chipBounds.minX - label.width / 2)
|
|
395
412
|
shiftedCoordinates.add(chipBounds.maxX + label.width / 2)
|
|
396
413
|
}
|
|
414
|
+
for (const otherTrace of this.traces) {
|
|
415
|
+
if (otherTrace.globalConnNetId === label.globalConnNetId) continue
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < otherTrace.tracePath.length - 1; i++) {
|
|
418
|
+
const start = otherTrace.tracePath[i]!
|
|
419
|
+
const end = otherTrace.tracePath[i + 1]!
|
|
420
|
+
if (!segmentCrossesBoundsInterior(start, end, labelBounds)) continue
|
|
421
|
+
|
|
422
|
+
const halfLabelWidth = label.width / 2
|
|
423
|
+
if (Math.abs(start.x - end.x) <= EPS) {
|
|
424
|
+
shiftedCoordinates.add(
|
|
425
|
+
start.x - halfLabelWidth - LABEL_TRACE_CLEARANCE,
|
|
426
|
+
)
|
|
427
|
+
shiftedCoordinates.add(
|
|
428
|
+
start.x + halfLabelWidth + LABEL_TRACE_CLEARANCE,
|
|
429
|
+
)
|
|
430
|
+
} else {
|
|
431
|
+
shiftedCoordinates.add(
|
|
432
|
+
Math.min(start.x, end.x) - halfLabelWidth - LABEL_TRACE_CLEARANCE,
|
|
433
|
+
)
|
|
434
|
+
shiftedCoordinates.add(
|
|
435
|
+
Math.max(start.x, end.x) + halfLabelWidth + LABEL_TRACE_CLEARANCE,
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
397
440
|
|
|
398
441
|
return [...shiftedCoordinates].flatMap((x) => {
|
|
399
442
|
if (Math.abs(x - corner.x) <= EPS) return []
|
|
@@ -30,6 +30,7 @@ import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/Ne
|
|
|
30
30
|
import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
|
|
31
31
|
import { UnroutedTraceRecoverySolver } from "../UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
32
32
|
import { SameNetJunctionAlignmentSolver } from "../SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver"
|
|
33
|
+
import { TraceElbowTransitionSimplificationSolver } from "../TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver"
|
|
33
34
|
|
|
34
35
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
35
36
|
solverName: string
|
|
@@ -88,6 +89,9 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
88
89
|
netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
|
|
89
90
|
traceCleanupSolver2?: TraceCleanupSolver
|
|
90
91
|
netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
|
|
92
|
+
traceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
93
|
+
preAlignmentTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
94
|
+
finalTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
91
95
|
sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver
|
|
92
96
|
|
|
93
97
|
startTimeOfPhase: Record<string, number>
|
|
@@ -224,9 +228,27 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
224
228
|
]
|
|
225
229
|
},
|
|
226
230
|
),
|
|
231
|
+
definePipelineStep(
|
|
232
|
+
"traceElbowTransitionSimplificationSolver",
|
|
233
|
+
TraceElbowTransitionSimplificationSolver,
|
|
234
|
+
(instance) => {
|
|
235
|
+
const overlapAvoidanceOutput =
|
|
236
|
+
instance.traceLabelOverlapAvoidanceSolver!.getOutput()
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
inputProblem: instance.inputProblem,
|
|
240
|
+
traces: overlapAvoidanceOutput.traces,
|
|
241
|
+
completedReroutes: overlapAvoidanceOutput.completedReroutes,
|
|
242
|
+
netLabelPlacements:
|
|
243
|
+
instance.traceLabelOverlapAvoidanceSolver!.netLabelPlacements,
|
|
244
|
+
paddingBuffer: 0.1,
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
),
|
|
227
249
|
definePipelineStep("traceCleanupSolver", TraceCleanupSolver, (instance) => {
|
|
228
250
|
const prevSolverOutput =
|
|
229
|
-
instance.
|
|
251
|
+
instance.traceElbowTransitionSimplificationSolver!.getOutput()
|
|
230
252
|
const traces = prevSolverOutput.traces
|
|
231
253
|
|
|
232
254
|
const labelMergingOutput =
|
|
@@ -348,12 +370,29 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
348
370
|
},
|
|
349
371
|
],
|
|
350
372
|
),
|
|
373
|
+
definePipelineStep(
|
|
374
|
+
"preAlignmentTraceElbowTransitionSimplificationSolver",
|
|
375
|
+
TraceElbowTransitionSimplificationSolver,
|
|
376
|
+
(instance) => {
|
|
377
|
+
const collisionOutput =
|
|
378
|
+
instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
inputProblem: instance.inputProblem,
|
|
382
|
+
traces: collisionOutput.traces,
|
|
383
|
+
completedReroutes: collisionOutput.completedReroutes,
|
|
384
|
+
netLabelPlacements: collisionOutput.netLabelPlacements,
|
|
385
|
+
paddingBuffer: 0.1,
|
|
386
|
+
},
|
|
387
|
+
]
|
|
388
|
+
},
|
|
389
|
+
),
|
|
351
390
|
definePipelineStep(
|
|
352
391
|
"traceCleanupSolver2",
|
|
353
392
|
TraceCleanupSolver,
|
|
354
393
|
(instance) => {
|
|
355
394
|
const collisionOutput =
|
|
356
|
-
instance.
|
|
395
|
+
instance.preAlignmentTraceElbowTransitionSimplificationSolver!.getOutput()
|
|
357
396
|
const labelMergingOutput =
|
|
358
397
|
instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
|
|
359
398
|
|
|
@@ -416,18 +455,37 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
416
455
|
]
|
|
417
456
|
},
|
|
418
457
|
),
|
|
458
|
+
definePipelineStep(
|
|
459
|
+
"finalTraceElbowTransitionSimplificationSolver",
|
|
460
|
+
TraceElbowTransitionSimplificationSolver,
|
|
461
|
+
(instance) => {
|
|
462
|
+
const collisionOutput =
|
|
463
|
+
instance.netLabelTraceCollisionSolver!.getOutput()
|
|
464
|
+
return [
|
|
465
|
+
{
|
|
466
|
+
inputProblem: instance.inputProblem,
|
|
467
|
+
traces: collisionOutput.traces,
|
|
468
|
+
completedReroutes: collisionOutput.completedReroutes,
|
|
469
|
+
netLabelPlacements: collisionOutput.netLabelPlacements,
|
|
470
|
+
paddingBuffer: 0.1,
|
|
471
|
+
},
|
|
472
|
+
]
|
|
473
|
+
},
|
|
474
|
+
),
|
|
419
475
|
definePipelineStep(
|
|
420
476
|
"netLabelNetLabelCollisionSolver",
|
|
421
477
|
NetLabelNetLabelCollisionSolver,
|
|
422
|
-
(instance) =>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
instance.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
478
|
+
(instance) => {
|
|
479
|
+
const simplificationOutput =
|
|
480
|
+
instance.finalTraceElbowTransitionSimplificationSolver!.getOutput()
|
|
481
|
+
return [
|
|
482
|
+
{
|
|
483
|
+
inputProblem: instance.inputProblem,
|
|
484
|
+
traces: simplificationOutput.traces,
|
|
485
|
+
netLabelPlacements: simplificationOutput.netLabelPlacements,
|
|
486
|
+
},
|
|
487
|
+
]
|
|
488
|
+
},
|
|
431
489
|
),
|
|
432
490
|
definePipelineStep(
|
|
433
491
|
"sameNetJunctionAlignmentSolver",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
7
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
8
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
9
|
+
import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
|
|
10
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
11
|
+
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
12
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
13
|
+
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
|
|
14
|
+
import type { CompletedTraceReroute } from "./types"
|
|
15
|
+
import { generateElbowTransitionSimplificationCandidates } from "./generateElbowTransitionSimplificationCandidates"
|
|
16
|
+
|
|
17
|
+
interface TraceElbowTransitionSimplificationSolverInput {
|
|
18
|
+
inputProblem: InputProblem
|
|
19
|
+
traces: SolvedTracePath[]
|
|
20
|
+
completedReroutes: CompletedTraceReroute[]
|
|
21
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
22
|
+
paddingBuffer: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PATH_LENGTH_EPSILON = 1e-9
|
|
26
|
+
|
|
27
|
+
const getPathLength = (points: Point[]) =>
|
|
28
|
+
points.slice(1).reduce((length, point, pointIndex) => {
|
|
29
|
+
const previousPoint = points[pointIndex]!
|
|
30
|
+
return (
|
|
31
|
+
length +
|
|
32
|
+
Math.abs(point.x - previousPoint.x) +
|
|
33
|
+
Math.abs(point.y - previousPoint.y)
|
|
34
|
+
)
|
|
35
|
+
}, 0)
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Post-processes traces after trace/label overlap avoidance. It removes
|
|
39
|
+
* redundant elbow transitions and, when needed, shifts a simplified elbow
|
|
40
|
+
* around a rendered label. Every replacement is revalidated against labels,
|
|
41
|
+
* components, other-net traces, and label anchors.
|
|
42
|
+
*/
|
|
43
|
+
export class TraceElbowTransitionSimplificationSolver extends BaseSolver {
|
|
44
|
+
private input: TraceElbowTransitionSimplificationSolverInput
|
|
45
|
+
private outputTraces: SolvedTracePath[]
|
|
46
|
+
private traceIdQueue: string[]
|
|
47
|
+
private obstacles: ReturnType<typeof getObstacleRects>
|
|
48
|
+
|
|
49
|
+
constructor(input: TraceElbowTransitionSimplificationSolverInput) {
|
|
50
|
+
super()
|
|
51
|
+
this.input = input
|
|
52
|
+
this.outputTraces = [...input.traces]
|
|
53
|
+
const reroutedTraceIds = new Set(
|
|
54
|
+
input.completedReroutes.map(
|
|
55
|
+
(completedReroute) => completedReroute.initialTrace.mspPairId,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
this.traceIdQueue = input.traces
|
|
59
|
+
.map((trace) => trace.mspPairId)
|
|
60
|
+
.filter((traceId) => reroutedTraceIds.has(traceId))
|
|
61
|
+
this.obstacles = getObstacleRects(input.inputProblem)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override _step() {
|
|
65
|
+
const traceId = this.traceIdQueue.shift()
|
|
66
|
+
if (!traceId) {
|
|
67
|
+
this.solved = true
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const traceIndex = this.outputTraces.findIndex(
|
|
72
|
+
(trace) => trace.mspPairId === traceId,
|
|
73
|
+
)
|
|
74
|
+
const trace = this.outputTraces[traceIndex]!
|
|
75
|
+
const tracePath = simplifyPath(trace.tracePath)
|
|
76
|
+
const initialOverlaps = detectTraceLabelOverlap({
|
|
77
|
+
traces: [{ ...trace, tracePath }],
|
|
78
|
+
netLabels: this.input.netLabelPlacements,
|
|
79
|
+
})
|
|
80
|
+
const otherNetTraces = this.outputTraces.filter(
|
|
81
|
+
(otherTrace) =>
|
|
82
|
+
otherTrace.mspPairId !== trace.mspPairId &&
|
|
83
|
+
otherTrace.globalConnNetId !== trace.globalConnNetId,
|
|
84
|
+
)
|
|
85
|
+
const candidateByPath = new Map<string, Point[]>()
|
|
86
|
+
const completedReroutes = this.input.completedReroutes.filter(
|
|
87
|
+
(completedReroute) =>
|
|
88
|
+
completedReroute.initialTrace.mspPairId === trace.mspPairId,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
for (const completedReroute of completedReroutes) {
|
|
92
|
+
const initialReroutePath = simplifyPath(
|
|
93
|
+
completedReroute.initialTrace.tracePath,
|
|
94
|
+
)
|
|
95
|
+
const reroutedPath = simplifyPath(completedReroute.reroutedTracePath)
|
|
96
|
+
const initialRerouteOverlapCount = detectTraceLabelOverlap({
|
|
97
|
+
traces: [
|
|
98
|
+
{ ...completedReroute.initialTrace, tracePath: initialReroutePath },
|
|
99
|
+
],
|
|
100
|
+
netLabels: this.input.netLabelPlacements,
|
|
101
|
+
}).length
|
|
102
|
+
|
|
103
|
+
const candidates = generateElbowTransitionSimplificationCandidates({
|
|
104
|
+
trace: completedReroute.initialTrace,
|
|
105
|
+
label: completedReroute.label,
|
|
106
|
+
netLabelPlacements: this.input.netLabelPlacements,
|
|
107
|
+
paddingBuffer: this.input.paddingBuffer,
|
|
108
|
+
detourCount: completedReroute.detourCount,
|
|
109
|
+
})
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
const simplifiedCandidate = simplifyPath(candidate)
|
|
112
|
+
const candidateTrace = {
|
|
113
|
+
...completedReroute.initialTrace,
|
|
114
|
+
tracePath: simplifiedCandidate,
|
|
115
|
+
}
|
|
116
|
+
const candidateOverlapCount = detectTraceLabelOverlap({
|
|
117
|
+
traces: [candidateTrace],
|
|
118
|
+
netLabels: this.input.netLabelPlacements,
|
|
119
|
+
}).length
|
|
120
|
+
const isSimplerEquivalentReroute =
|
|
121
|
+
candidateOverlapCount < initialRerouteOverlapCount &&
|
|
122
|
+
Math.abs(
|
|
123
|
+
getPathLength(simplifiedCandidate) - getPathLength(reroutedPath),
|
|
124
|
+
) < PATH_LENGTH_EPSILON &&
|
|
125
|
+
simplifiedCandidate.length < reroutedPath.length &&
|
|
126
|
+
preservesLabelAnchors(
|
|
127
|
+
this.input.netLabelPlacements,
|
|
128
|
+
[completedReroute.initialTrace],
|
|
129
|
+
[candidateTrace],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if (isSimplerEquivalentReroute) {
|
|
133
|
+
candidateByPath.set(
|
|
134
|
+
simplifiedCandidate
|
|
135
|
+
.map((point) => `${point.x},${point.y}`)
|
|
136
|
+
.join(";"),
|
|
137
|
+
simplifiedCandidate,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const initialOverlapIds = new Set(
|
|
144
|
+
initialOverlaps.map(
|
|
145
|
+
({ label }) => `${label.globalConnNetId}:${label.netId}`,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
const initialPathLength = getPathLength(tracePath)
|
|
149
|
+
|
|
150
|
+
const validCandidates = [...candidateByPath.values()].filter(
|
|
151
|
+
(candidatePath) => {
|
|
152
|
+
const candidateTrace = { ...trace, tracePath: candidatePath }
|
|
153
|
+
const candidateOverlaps = detectTraceLabelOverlap({
|
|
154
|
+
traces: [candidateTrace],
|
|
155
|
+
netLabels: this.input.netLabelPlacements,
|
|
156
|
+
})
|
|
157
|
+
const candidateOnlyKeepsExistingOverlaps = candidateOverlaps.every(
|
|
158
|
+
({ label }) =>
|
|
159
|
+
initialOverlapIds.has(`${label.globalConnNetId}:${label.netId}`),
|
|
160
|
+
)
|
|
161
|
+
const reducesCollisions =
|
|
162
|
+
candidateOverlaps.length < initialOverlaps.length
|
|
163
|
+
const simplifiesGeometry =
|
|
164
|
+
candidateOverlaps.length === initialOverlaps.length &&
|
|
165
|
+
getPathLength(candidatePath) <=
|
|
166
|
+
initialPathLength + PATH_LENGTH_EPSILON &&
|
|
167
|
+
candidatePath.length < tracePath.length
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
candidateOnlyKeepsExistingOverlaps &&
|
|
171
|
+
(reducesCollisions || simplifiesGeometry) &&
|
|
172
|
+
preservesLabelAnchors(
|
|
173
|
+
this.input.netLabelPlacements,
|
|
174
|
+
[trace],
|
|
175
|
+
[candidateTrace],
|
|
176
|
+
) &&
|
|
177
|
+
!isPathCollidingWithObstacles(candidatePath, this.obstacles) &&
|
|
178
|
+
!doesPathCoincideWithTraces(candidatePath, otherNetTraces)
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const getOverlapCount = (candidatePath: Point[]) =>
|
|
184
|
+
detectTraceLabelOverlap({
|
|
185
|
+
traces: [{ ...trace, tracePath: candidatePath }],
|
|
186
|
+
netLabels: this.input.netLabelPlacements,
|
|
187
|
+
}).length
|
|
188
|
+
validCandidates.sort((a, b) => {
|
|
189
|
+
const overlapDifference = getOverlapCount(a) - getOverlapCount(b)
|
|
190
|
+
if (overlapDifference !== 0) return overlapDifference
|
|
191
|
+
return getPathLength(a) - getPathLength(b) || a.length - b.length
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const bestCandidate = validCandidates[0]
|
|
195
|
+
if (!bestCandidate) return
|
|
196
|
+
|
|
197
|
+
this.outputTraces[traceIndex] = { ...trace, tracePath: bestCandidate }
|
|
198
|
+
this.stats.simplifiedTraceCount = (this.stats.simplifiedTraceCount ?? 0) + 1
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getOutput() {
|
|
202
|
+
return {
|
|
203
|
+
traces: this.outputTraces,
|
|
204
|
+
netLabelPlacements: this.input.netLabelPlacements,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override visualize(): GraphicsObject {
|
|
209
|
+
const graphics = visualizeInputProblem(this.input.inputProblem)
|
|
210
|
+
graphics.lines ??= []
|
|
211
|
+
for (const trace of this.outputTraces) {
|
|
212
|
+
graphics.lines.push({ points: trace.tracePath, strokeColor: "purple" })
|
|
213
|
+
}
|
|
214
|
+
return graphics
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import {
|
|
6
|
+
isHorizontal,
|
|
7
|
+
isVertical,
|
|
8
|
+
segmentIntersectsRect,
|
|
9
|
+
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
10
|
+
import { shiftSegmentOrth } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps"
|
|
11
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
12
|
+
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
13
|
+
|
|
14
|
+
export const isSimpleFiveSegmentElbow = (path: Point[]): boolean => {
|
|
15
|
+
const simplifiedPath = simplifyPath(path)
|
|
16
|
+
if (simplifiedPath.length !== 6) return false
|
|
17
|
+
|
|
18
|
+
const segmentIsHorizontal = simplifiedPath
|
|
19
|
+
.slice(0, -1)
|
|
20
|
+
.map((point, index) => isHorizontal(point, simplifiedPath[index + 1]!))
|
|
21
|
+
|
|
22
|
+
return segmentIsHorizontal.every(
|
|
23
|
+
(isHorizontalSegment, index) =>
|
|
24
|
+
index === 0 || isHorizontalSegment !== segmentIsHorizontal[index - 1],
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const generateSegmentShiftCandidates = ({
|
|
29
|
+
trace,
|
|
30
|
+
label,
|
|
31
|
+
paddingBuffer,
|
|
32
|
+
detourCount,
|
|
33
|
+
}: {
|
|
34
|
+
trace: SolvedTracePath
|
|
35
|
+
label: NetLabelPlacement
|
|
36
|
+
paddingBuffer: number
|
|
37
|
+
detourCount: number
|
|
38
|
+
}): Point[][] => {
|
|
39
|
+
if (trace.globalConnNetId === label.globalConnNetId) return []
|
|
40
|
+
|
|
41
|
+
const path = simplifyPath(trace.tracePath)
|
|
42
|
+
if (!isSimpleFiveSegmentElbow(path)) return []
|
|
43
|
+
|
|
44
|
+
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
45
|
+
const effectivePadding = paddingBuffer + detourCount * paddingBuffer
|
|
46
|
+
const paddedLabelBounds = {
|
|
47
|
+
minX: labelBounds.minX - effectivePadding,
|
|
48
|
+
maxX: labelBounds.maxX + effectivePadding,
|
|
49
|
+
minY: labelBounds.minY - effectivePadding,
|
|
50
|
+
maxY: labelBounds.maxY + effectivePadding,
|
|
51
|
+
}
|
|
52
|
+
const candidates: Point[][] = []
|
|
53
|
+
|
|
54
|
+
for (let segmentIndex = 1; segmentIndex < path.length - 2; segmentIndex++) {
|
|
55
|
+
const segmentStart = path[segmentIndex]!
|
|
56
|
+
const segmentEnd = path[segmentIndex + 1]!
|
|
57
|
+
if (!segmentIntersectsRect(segmentStart, segmentEnd, labelBounds)) continue
|
|
58
|
+
|
|
59
|
+
const isHorizontalSegment = isHorizontal(segmentStart, segmentEnd)
|
|
60
|
+
const isVerticalSegment = isVertical(segmentStart, segmentEnd)
|
|
61
|
+
if (!isHorizontalSegment && !isVerticalSegment) continue
|
|
62
|
+
|
|
63
|
+
const axis = isHorizontalSegment ? "y" : "x"
|
|
64
|
+
const coordinates = isHorizontalSegment
|
|
65
|
+
? [paddedLabelBounds.minY, paddedLabelBounds.maxY]
|
|
66
|
+
: [paddedLabelBounds.minX, paddedLabelBounds.maxX]
|
|
67
|
+
|
|
68
|
+
for (const coordinate of coordinates) {
|
|
69
|
+
const shiftedPath = shiftSegmentOrth(path, segmentIndex, axis, coordinate)
|
|
70
|
+
if (shiftedPath) candidates.push(shiftedPath)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return candidates
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const generateTransitionShiftCandidates = ({
|
|
78
|
+
trace,
|
|
79
|
+
label,
|
|
80
|
+
paddingBuffer,
|
|
81
|
+
detourCount,
|
|
82
|
+
}: {
|
|
83
|
+
trace: SolvedTracePath
|
|
84
|
+
label: NetLabelPlacement
|
|
85
|
+
paddingBuffer: number
|
|
86
|
+
detourCount: number
|
|
87
|
+
}): Point[][] => {
|
|
88
|
+
const path = simplifyPath(trace.tracePath)
|
|
89
|
+
if (!isSimpleFiveSegmentElbow(path)) return []
|
|
90
|
+
|
|
91
|
+
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
92
|
+
const effectivePadding = paddingBuffer + detourCount * paddingBuffer
|
|
93
|
+
const paddedLabelBounds = {
|
|
94
|
+
minX: labelBounds.minX - effectivePadding,
|
|
95
|
+
maxX: labelBounds.maxX + effectivePadding,
|
|
96
|
+
minY: labelBounds.minY - effectivePadding,
|
|
97
|
+
maxY: labelBounds.maxY + effectivePadding,
|
|
98
|
+
}
|
|
99
|
+
const start = path[0]!
|
|
100
|
+
const end = path[path.length - 1]!
|
|
101
|
+
const middleSegmentIndex = 2
|
|
102
|
+
const candidates: Point[][] = []
|
|
103
|
+
|
|
104
|
+
for (let segmentIndex = 0; segmentIndex < path.length - 1; segmentIndex++) {
|
|
105
|
+
const segmentStart = path[segmentIndex]!
|
|
106
|
+
const segmentEnd = path[segmentIndex + 1]!
|
|
107
|
+
if (!segmentIntersectsRect(segmentStart, segmentEnd, labelBounds)) continue
|
|
108
|
+
if (Math.abs(segmentIndex - middleSegmentIndex) !== 1) continue
|
|
109
|
+
|
|
110
|
+
const isHorizontalSegment = isHorizontal(segmentStart, segmentEnd)
|
|
111
|
+
const isVerticalSegment = isVertical(segmentStart, segmentEnd)
|
|
112
|
+
if (!isHorizontalSegment && !isVerticalSegment) continue
|
|
113
|
+
|
|
114
|
+
const axis = isHorizontalSegment ? "x" : "y"
|
|
115
|
+
const min = isHorizontalSegment
|
|
116
|
+
? paddedLabelBounds.minX
|
|
117
|
+
: paddedLabelBounds.minY
|
|
118
|
+
const max = isHorizontalSegment
|
|
119
|
+
? paddedLabelBounds.maxX
|
|
120
|
+
: paddedLabelBounds.maxY
|
|
121
|
+
const startCoordinate = isHorizontalSegment ? start.x : start.y
|
|
122
|
+
const endCoordinate = isHorizontalSegment ? end.x : end.y
|
|
123
|
+
const corridorCoordinates: number[] = []
|
|
124
|
+
|
|
125
|
+
if (startCoordinate < min) {
|
|
126
|
+
corridorCoordinates.push((startCoordinate + min) / 2)
|
|
127
|
+
} else if (startCoordinate > max) {
|
|
128
|
+
corridorCoordinates.push((startCoordinate + max) / 2)
|
|
129
|
+
}
|
|
130
|
+
if (endCoordinate < min) {
|
|
131
|
+
corridorCoordinates.push((endCoordinate + min) / 2)
|
|
132
|
+
} else if (endCoordinate > max) {
|
|
133
|
+
corridorCoordinates.push((endCoordinate + max) / 2)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const coordinate of new Set(corridorCoordinates)) {
|
|
137
|
+
const shiftedPath = shiftSegmentOrth(
|
|
138
|
+
path,
|
|
139
|
+
middleSegmentIndex,
|
|
140
|
+
axis,
|
|
141
|
+
coordinate,
|
|
142
|
+
)
|
|
143
|
+
if (shiftedPath) candidates.push(shiftedPath)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return candidates
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const generateElbowTransitionSimplificationCandidates = ({
|
|
151
|
+
trace,
|
|
152
|
+
label,
|
|
153
|
+
netLabelPlacements,
|
|
154
|
+
paddingBuffer,
|
|
155
|
+
detourCount,
|
|
156
|
+
}: {
|
|
157
|
+
trace: SolvedTracePath
|
|
158
|
+
label: NetLabelPlacement
|
|
159
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
160
|
+
paddingBuffer: number
|
|
161
|
+
detourCount: number
|
|
162
|
+
}): Point[][] => {
|
|
163
|
+
const transitionCandidates = generateTransitionShiftCandidates({
|
|
164
|
+
trace,
|
|
165
|
+
label,
|
|
166
|
+
paddingBuffer,
|
|
167
|
+
detourCount,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return transitionCandidates.flatMap((tracePath) => {
|
|
171
|
+
const shiftedTrace = { ...trace, tracePath }
|
|
172
|
+
const shiftedOverlaps = detectTraceLabelOverlap({
|
|
173
|
+
traces: [shiftedTrace],
|
|
174
|
+
netLabels: netLabelPlacements,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return shiftedOverlaps.flatMap(({ label: shiftedLabel }) =>
|
|
178
|
+
generateSegmentShiftCandidates({
|
|
179
|
+
trace: shiftedTrace,
|
|
180
|
+
label: shiftedLabel,
|
|
181
|
+
paddingBuffer,
|
|
182
|
+
detourCount,
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
|
|
5
|
+
export interface CompletedTraceReroute {
|
|
6
|
+
initialTrace: SolvedTracePath
|
|
7
|
+
reroutedTracePath: Point[]
|
|
8
|
+
label: NetLabelPlacement
|
|
9
|
+
detourCount: number
|
|
10
|
+
}
|