@tscircuit/schematic-trace-solver 0.0.45 → 0.0.47
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/.github/workflows/bun-formatcheck.yml +1 -1
- package/.github/workflows/bun-pver-release.yml +1 -1
- package/.github/workflows/bun-test.yml +1 -1
- package/.github/workflows/bun-typecheck.yml +1 -1
- package/dist/index.d.ts +35 -9
- package/dist/index.js +543 -186
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +92 -50
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +26 -11
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +16 -5
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +95 -75
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts +74 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +45 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/mergeLabelGroup.ts +71 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +162 -28
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/doesTraceStartOrEndInLabel.ts +58 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/isPointInsideLabel.ts +23 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts +54 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +13 -1
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +110 -60
- package/package.json +1 -1
- package/site/examples/example27.page.tsx +4 -0
- package/tests/assets/example27.json +168 -0
- package/tests/examples/__snapshots__/example03.snap.svg +67 -67
- package/tests/examples/__snapshots__/example16.snap.svg +3 -3
- package/tests/examples/__snapshots__/example30.snap.svg +258 -0
- package/tests/examples/example30.test.ts +12 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +4 -1
|
@@ -7,6 +7,7 @@ import type { InputProblem } from "../../types/InputProblem"
|
|
|
7
7
|
import { MergedNetLabelObstacleSolver } from "./sub-solvers/LabelMergingSolver/LabelMergingSolver"
|
|
8
8
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
9
9
|
import { OverlapAvoidanceStepSolver } from "./sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver"
|
|
10
|
+
import { detectTraceLabelOverlap } from "./detectTraceLabelOverlap"
|
|
10
11
|
|
|
11
12
|
interface TraceLabelOverlapAvoidanceSolverInput {
|
|
12
13
|
inputProblem: InputProblem
|
|
@@ -15,73 +16,110 @@ interface TraceLabelOverlapAvoidanceSolverInput {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* Resolves overlaps between schematic traces and net labels using a two-phase,
|
|
20
|
+
* "fire-and-forget" dispatching strategy.
|
|
21
|
+
*
|
|
22
|
+
* This solver operates in two distinct phases:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Dispatch Phase**: Iterates through traces, identifying clean ones and dispatching
|
|
25
|
+
* colliding ones to dedicated `OverlapAvoidanceStepSolver` instances.
|
|
26
|
+
*
|
|
27
|
+
* 2. **Execution Phase**: Steps through all dispatched sub-solvers until they complete.
|
|
28
|
+
*
|
|
29
|
+
* The final output combines clean traces with results from sub-solvers. A final
|
|
30
|
+
* `MergedNetLabelObstacleSolver` ensures pipeline compatibility.
|
|
20
31
|
*/
|
|
21
32
|
export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
22
33
|
inputProblem: InputProblem
|
|
23
|
-
traces: SolvedTracePath[]
|
|
24
34
|
netLabelPlacements: NetLabelPlacement[]
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
unprocessedTraces: SolvedTracePath[] = []
|
|
37
|
+
cleanTraces: SolvedTracePath[] = []
|
|
38
|
+
subSolvers: OverlapAvoidanceStepSolver[] = []
|
|
39
|
+
private phase: "searching_for_overlaps" | "fixing_overlaps" =
|
|
40
|
+
"searching_for_overlaps"
|
|
41
|
+
detourCounts: Map<string, number> = new Map()
|
|
42
|
+
|
|
27
43
|
labelMergingSolver?: MergedNetLabelObstacleSolver
|
|
28
|
-
overlapAvoidanceSolver?: OverlapAvoidanceStepSolver
|
|
29
|
-
pipelineStepIndex = 0
|
|
30
44
|
|
|
31
45
|
constructor(solverInput: TraceLabelOverlapAvoidanceSolverInput) {
|
|
32
46
|
super()
|
|
33
47
|
this.inputProblem = solverInput.inputProblem
|
|
34
|
-
this.
|
|
48
|
+
this.unprocessedTraces = [...solverInput.traces]
|
|
35
49
|
this.netLabelPlacements = solverInput.netLabelPlacements
|
|
50
|
+
this.cleanTraces = []
|
|
51
|
+
this.subSolvers = []
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
override _step() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
} else if (this.activeSubSolver.failed) {
|
|
47
|
-
this.failed = true // If any sub-solver fails, the whole thing fails
|
|
48
|
-
this.activeSubSolver = null
|
|
55
|
+
if (this.phase === "searching_for_overlaps") {
|
|
56
|
+
if (this.unprocessedTraces.length === 0) {
|
|
57
|
+
console.log(
|
|
58
|
+
`Dispatch phase complete. Created ${this.subSolvers.length} sub-solvers.`,
|
|
59
|
+
)
|
|
60
|
+
this.phase = "fixing_overlaps"
|
|
61
|
+
return
|
|
49
62
|
}
|
|
50
|
-
return // Return to allow the sub-solver to run
|
|
51
|
-
}
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
const currentTargetTrace = this.unprocessedTraces.shift()!
|
|
65
|
+
|
|
66
|
+
const localOverlaps = detectTraceLabelOverlap({
|
|
67
|
+
traces: [currentTargetTrace],
|
|
68
|
+
netLabels: this.netLabelPlacements,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (localOverlaps.length === 0) {
|
|
72
|
+
this.cleanTraces.push(currentTargetTrace)
|
|
73
|
+
} else {
|
|
74
|
+
// Dispatch a new sub-solver for this dirty trace
|
|
75
|
+
const collidingLabels = localOverlaps.map((o) => o.label)
|
|
76
|
+
const labelMerger = new MergedNetLabelObstacleSolver({
|
|
77
|
+
netLabelPlacements: collidingLabels,
|
|
58
78
|
inputProblem: this.inputProblem,
|
|
59
|
-
traces:
|
|
79
|
+
traces: [currentTargetTrace],
|
|
60
80
|
})
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
labelMerger.solve()
|
|
82
|
+
const mergingOutput = labelMerger.getOutput()
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
this.overlapAvoidanceSolver = new OverlapAvoidanceStepSolver({
|
|
84
|
+
const subSolver = new OverlapAvoidanceStepSolver({
|
|
66
85
|
inputProblem: this.inputProblem,
|
|
67
|
-
traces:
|
|
68
|
-
netLabelPlacements
|
|
69
|
-
|
|
70
|
-
mergedLabelNetIdMap:
|
|
71
|
-
|
|
86
|
+
traces: [currentTargetTrace],
|
|
87
|
+
initialNetLabelPlacements: this.netLabelPlacements,
|
|
88
|
+
mergedNetLabelPlacements: mergingOutput.netLabelPlacements,
|
|
89
|
+
mergedLabelNetIdMap: mergingOutput.mergedLabelNetIdMap,
|
|
90
|
+
detourCounts: this.detourCounts,
|
|
72
91
|
})
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
this.subSolvers.push(subSolver)
|
|
93
|
+
}
|
|
94
|
+
} else if (this.phase === "fixing_overlaps") {
|
|
95
|
+
if (this.subSolvers.every((s) => s.solved || s.failed)) {
|
|
96
|
+
console.log("All sub-solvers finished.")
|
|
97
|
+
// Final merge for pipeline compatibility
|
|
98
|
+
if (!this.labelMergingSolver) {
|
|
99
|
+
this.labelMergingSolver = new MergedNetLabelObstacleSolver({
|
|
100
|
+
netLabelPlacements: this.netLabelPlacements,
|
|
101
|
+
inputProblem: this.inputProblem,
|
|
102
|
+
traces: this.getOutput().traces,
|
|
103
|
+
})
|
|
104
|
+
this.labelMergingSolver.solve()
|
|
105
|
+
}
|
|
77
106
|
this.solved = true
|
|
78
|
-
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Step through all active sub-solvers
|
|
111
|
+
for (const solver of this.subSolvers) {
|
|
112
|
+
if (!solver.solved && !solver.failed) {
|
|
113
|
+
solver.step()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
getOutput() {
|
|
120
|
+
const solvedTraces = this.subSolvers.flatMap((s) => s.getOutput().allTraces)
|
|
83
121
|
return {
|
|
84
|
-
traces: this.
|
|
122
|
+
traces: [...this.cleanTraces, ...solvedTraces],
|
|
85
123
|
netLabelPlacements:
|
|
86
124
|
this.labelMergingSolver?.getOutput().netLabelPlacements ??
|
|
87
125
|
this.netLabelPlacements,
|
|
@@ -89,26 +127,30 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
|
89
127
|
}
|
|
90
128
|
|
|
91
129
|
override visualize(): GraphicsObject {
|
|
92
|
-
if (this.activeSubSolver) {
|
|
93
|
-
return this.activeSubSolver.visualize()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// When no sub-solver is active, show the current state of the pipeline
|
|
97
130
|
const graphics = visualizeInputProblem(this.inputProblem)
|
|
98
131
|
if (!graphics.lines) graphics.lines = []
|
|
99
132
|
if (!graphics.rects) graphics.rects = []
|
|
100
133
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const trace of output.traces) {
|
|
134
|
+
// Show clean traces in purple
|
|
135
|
+
for (const trace of this.cleanTraces) {
|
|
104
136
|
graphics.lines!.push({
|
|
105
137
|
points: trace.tracePath,
|
|
106
138
|
strokeColor: "purple",
|
|
107
139
|
})
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
// Delegate visualization to sub-solvers
|
|
143
|
+
for (const solver of this.subSolvers) {
|
|
144
|
+
const solverGraphics = solver.visualize()
|
|
145
|
+
graphics.lines!.push(...(solverGraphics.lines ?? []))
|
|
146
|
+
graphics.rects!.push(...(solverGraphics.rects ?? []))
|
|
147
|
+
// graphics.texts!.push(...(solverGraphics.texts ?? []))
|
|
148
|
+
graphics.points!.push(...(solverGraphics.points ?? []))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Also show original labels
|
|
152
|
+
for (const label of this.netLabelPlacements) {
|
|
153
|
+
const color = getColorFromString(label.globalConnNetId, 0.3) // Make fill opaque
|
|
112
154
|
graphics.rects!.push({
|
|
113
155
|
center: label.center,
|
|
114
156
|
width: label.width,
|
|
@@ -8,28 +8,43 @@ export interface TraceLabelOverlap {
|
|
|
8
8
|
label: NetLabelPlacement
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
11
|
+
/**
|
|
12
|
+
* Detects overlaps between a set of traces and a set of net labels.
|
|
13
|
+
* It identifies instances where a trace segment intersects with a label's bounding box.
|
|
14
|
+
* Self-attachments (where a trace and label belong to the same net) are explicitly ignored
|
|
15
|
+
* as they are not considered true overlaps for avoidance purposes.
|
|
16
|
+
*
|
|
17
|
+
* @param traces - An array of SolvedTracePath objects to check for overlaps.
|
|
18
|
+
* @param netLabels - An array of NetLabelPlacement objects representing the labels.
|
|
19
|
+
* @returns An array of TraceLabelOverlap objects, each indicating an overlap
|
|
20
|
+
* between a trace and a label.
|
|
21
|
+
*/
|
|
22
|
+
export const detectTraceLabelOverlap = ({
|
|
23
|
+
traces,
|
|
24
|
+
netLabels = [],
|
|
25
|
+
}: {
|
|
26
|
+
traces: SolvedTracePath[]
|
|
27
|
+
netLabels?: NetLabelPlacement[]
|
|
28
|
+
}): TraceLabelOverlap[] => {
|
|
15
29
|
const overlaps: TraceLabelOverlap[] = []
|
|
16
30
|
|
|
17
31
|
for (const trace of traces) {
|
|
18
32
|
for (const label of netLabels) {
|
|
19
33
|
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
20
34
|
|
|
21
|
-
for (let
|
|
22
|
-
const p1 = trace.tracePath[
|
|
23
|
-
const p2 = trace.tracePath[
|
|
35
|
+
for (let j = 0; j < trace.tracePath.length - 1; j++) {
|
|
36
|
+
const p1 = trace.tracePath[j]
|
|
37
|
+
const p2 = trace.tracePath[j + 1]
|
|
24
38
|
|
|
25
39
|
if (segmentIntersectsRect(p1, p2, labelBounds)) {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
40
|
+
// If the trace and label belong to the same net, it's a legitimate connection, not an overlap to avoid.
|
|
41
|
+
// This check is now redundant with the new logic in OverlapAvoidanceStepSolver,
|
|
42
|
+
// but kept here for now as per instruction not to remove anything unnecessary.
|
|
28
43
|
if (trace.globalConnNetId === label.globalConnNetId) {
|
|
29
|
-
break //
|
|
44
|
+
break // Break from the inner-most loop (segment loop)
|
|
30
45
|
}
|
|
31
46
|
overlaps.push({ trace, label })
|
|
32
|
-
break //
|
|
47
|
+
break // Break from the inner-most loop (segment loop)
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
}
|
|
@@ -8,6 +8,18 @@ import { generateSnipAndReconnectCandidates } from "./trySnipAndReconnect"
|
|
|
8
8
|
import { generateFourPointDetourCandidates } from "./tryFourPointDetour"
|
|
9
9
|
import { simplifyPath } from "../TraceCleanupSolver/simplifyPath"
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Generates a list of candidate rerouted paths for a given trace that is
|
|
13
|
+
* colliding with a net label.
|
|
14
|
+
*
|
|
15
|
+
* This function employs multiple strategies to propose alternative paths:
|
|
16
|
+
* 1. **Four-Point Detour:** Creates a rectangular detour around the label.
|
|
17
|
+
* 2. **Snip and Reconnect:** Attempts to remove the colliding segment and
|
|
18
|
+
* reconnect the trace around the obstacle.
|
|
19
|
+
*
|
|
20
|
+
* The candidates are generated with increasing padding based on `detourCount`
|
|
21
|
+
* to explore progressively wider detours.
|
|
22
|
+
*/
|
|
11
23
|
export const generateRerouteCandidates = ({
|
|
12
24
|
trace,
|
|
13
25
|
label,
|
|
@@ -26,13 +38,12 @@ export const generateRerouteCandidates = ({
|
|
|
26
38
|
return [initialTrace.tracePath]
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
const labelPadding = paddingBuffer
|
|
30
41
|
const labelBoundsRaw = getRectBounds(label.center, label.width, label.height)
|
|
31
42
|
const labelBounds = {
|
|
32
|
-
minX: labelBoundsRaw.minX
|
|
33
|
-
minY: labelBoundsRaw.minY
|
|
34
|
-
maxX: labelBoundsRaw.maxX
|
|
35
|
-
maxY: labelBoundsRaw.maxY
|
|
43
|
+
minX: labelBoundsRaw.minX,
|
|
44
|
+
minY: labelBoundsRaw.minY,
|
|
45
|
+
maxX: labelBoundsRaw.maxX,
|
|
46
|
+
maxY: labelBoundsRaw.maxY,
|
|
36
47
|
chipId: `netlabel-${label.netId}`,
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -6,6 +6,9 @@ import type { GraphicsObject, Line } from "graphics-debug"
|
|
|
6
6
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
7
7
|
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
8
8
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
9
|
+
import { groupLabelsByChipAndOrientation } from "./groupLabelsByChipAndOrientation"
|
|
10
|
+
import { mergeLabelGroup } from "./mergeLabelGroup"
|
|
11
|
+
import { filterLabelsAtTraceEdges } from "./filterLabelsAtTraceEdges"
|
|
9
12
|
|
|
10
13
|
interface LabelMergingSolverInput {
|
|
11
14
|
netLabelPlacements: NetLabelPlacement[]
|
|
@@ -18,9 +21,15 @@ interface LabelMergingSolverOutput {
|
|
|
18
21
|
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
type PipelineStep =
|
|
25
|
+
| "filtering_labels"
|
|
26
|
+
| "grouping_labels"
|
|
27
|
+
| "merging_groups"
|
|
28
|
+
| "finalizing"
|
|
29
|
+
|
|
21
30
|
/**
|
|
22
31
|
* Merges multiple net labels into a single, larger label if they are on the
|
|
23
|
-
* same side of the same chip
|
|
32
|
+
* same side of the same chip and physically adjacent.
|
|
24
33
|
*/
|
|
25
34
|
export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
26
35
|
private input: LabelMergingSolverInput
|
|
@@ -28,15 +37,21 @@ export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
|
28
37
|
private inputProblem: InputProblem
|
|
29
38
|
private traces: SolvedTracePath[]
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
// State for the new pipeline
|
|
41
|
+
private pipelineStep: PipelineStep = "filtering_labels"
|
|
42
|
+
private filteredLabels: NetLabelPlacement[] = []
|
|
43
|
+
private labelGroups: Record<string, NetLabelPlacement[]> = {}
|
|
44
|
+
private groupKeysToProcess: string[] = []
|
|
45
|
+
private finalPlacements: NetLabelPlacement[] = []
|
|
46
|
+
private mergedLabelNetIdMap: Record<string, Set<string>> = {}
|
|
47
|
+
private activeMergingGroupKey: string | null = null
|
|
33
48
|
|
|
49
|
+
constructor(solverInput: LabelMergingSolverInput) {
|
|
34
50
|
super()
|
|
35
51
|
this.input = solverInput
|
|
36
52
|
this.inputProblem = solverInput.inputProblem
|
|
37
53
|
this.traces = solverInput.traces
|
|
38
54
|
|
|
39
|
-
// Initialize output to a default state to allow visualization before the first step
|
|
40
55
|
this.output = {
|
|
41
56
|
netLabelPlacements: solverInput.netLabelPlacements,
|
|
42
57
|
mergedLabelNetIdMap: {},
|
|
@@ -44,75 +59,67 @@ export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
override _step() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.solved = true
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const labelGroups: Record<string, NetLabelPlacement[]> = {}
|
|
60
|
-
|
|
61
|
-
for (const p of originalLabels) {
|
|
62
|
-
if (p.pinIds.length === 0) continue
|
|
63
|
-
const chipId = p.pinIds[0].split(".")[0]
|
|
64
|
-
if (!chipId) continue
|
|
65
|
-
const key = `${chipId}-${p.orientation}`
|
|
66
|
-
if (!(key in labelGroups)) {
|
|
67
|
-
labelGroups[key] = []
|
|
68
|
-
}
|
|
69
|
-
labelGroups[key]!.push(p)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const finalPlacements: NetLabelPlacement[] = []
|
|
73
|
-
for (const [key, group] of Object.entries(labelGroups)) {
|
|
74
|
-
if (group.length <= 1) {
|
|
75
|
-
finalPlacements.push(...group)
|
|
76
|
-
continue
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let minX = Infinity
|
|
80
|
-
let minY = Infinity
|
|
81
|
-
let maxX = -Infinity
|
|
82
|
-
let maxY = -Infinity
|
|
83
|
-
for (const p of group) {
|
|
84
|
-
const bounds = getRectBounds(p.center, p.width, p.height)
|
|
85
|
-
minX = Math.min(minX, bounds.minX)
|
|
86
|
-
minY = Math.min(minY, bounds.minY)
|
|
87
|
-
maxX = Math.max(maxX, bounds.maxX)
|
|
88
|
-
maxY = Math.max(maxY, bounds.maxY)
|
|
89
|
-
}
|
|
62
|
+
switch (this.pipelineStep) {
|
|
63
|
+
case "filtering_labels":
|
|
64
|
+
this.filteredLabels = filterLabelsAtTraceEdges({
|
|
65
|
+
labels: this.input.netLabelPlacements,
|
|
66
|
+
traces: this.traces,
|
|
67
|
+
})
|
|
68
|
+
this.pipelineStep = "grouping_labels"
|
|
69
|
+
break
|
|
90
70
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
...new Set(group.flatMap((p) => p.mspConnectionPairIds)),
|
|
107
|
-
],
|
|
108
|
-
})
|
|
109
|
-
}
|
|
71
|
+
case "grouping_labels":
|
|
72
|
+
this.labelGroups = groupLabelsByChipAndOrientation({
|
|
73
|
+
labels: this.filteredLabels,
|
|
74
|
+
chips: this.inputProblem.chips,
|
|
75
|
+
})
|
|
76
|
+
this.groupKeysToProcess = Object.keys(this.labelGroups)
|
|
77
|
+
this.pipelineStep = "merging_groups"
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
case "merging_groups":
|
|
81
|
+
if (this.groupKeysToProcess.length === 0) {
|
|
82
|
+
this.pipelineStep = "finalizing"
|
|
83
|
+
this.activeMergingGroupKey = null
|
|
84
|
+
break
|
|
85
|
+
}
|
|
110
86
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
87
|
+
const groupKey = this.groupKeysToProcess.pop()!
|
|
88
|
+
this.activeMergingGroupKey = groupKey
|
|
89
|
+
const group = this.labelGroups[groupKey]!
|
|
90
|
+
|
|
91
|
+
if (group.length > 1) {
|
|
92
|
+
const { mergedLabel, originalNetIds } = mergeLabelGroup(
|
|
93
|
+
group,
|
|
94
|
+
groupKey,
|
|
95
|
+
)
|
|
96
|
+
this.finalPlacements.push(mergedLabel)
|
|
97
|
+
this.mergedLabelNetIdMap[mergedLabel.globalConnNetId] = originalNetIds
|
|
98
|
+
} else {
|
|
99
|
+
this.finalPlacements.push(...group)
|
|
100
|
+
}
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
case "finalizing":
|
|
104
|
+
// Any labels that were filtered out and not part of any group should be added back
|
|
105
|
+
const processedOriginalIds = new Set(
|
|
106
|
+
this.finalPlacements.flatMap((p) =>
|
|
107
|
+
this.mergedLabelNetIdMap[p.globalConnNetId]
|
|
108
|
+
? [...this.mergedLabelNetIdMap[p.globalConnNetId]!]
|
|
109
|
+
: [p.globalConnNetId],
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
const unprocessedLabels = this.input.netLabelPlacements.filter(
|
|
113
|
+
(l) => !processedOriginalIds.has(l.globalConnNetId),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
this.output = {
|
|
117
|
+
netLabelPlacements: [...this.finalPlacements, ...unprocessedLabels],
|
|
118
|
+
mergedLabelNetIdMap: this.mergedLabelNetIdMap,
|
|
119
|
+
}
|
|
120
|
+
this.solved = true
|
|
121
|
+
break
|
|
114
122
|
}
|
|
115
|
-
this.solved = true
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
getOutput(): LabelMergingSolverOutput {
|
|
@@ -143,17 +150,33 @@ export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
|
143
150
|
graphics.lines!.push(line)
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
// Highlight the active sub-group being merged
|
|
154
|
+
if (
|
|
155
|
+
this.activeMergingGroupKey &&
|
|
156
|
+
this.labelGroups[this.activeMergingGroupKey]
|
|
157
|
+
) {
|
|
158
|
+
const activeGroup = this.labelGroups[this.activeMergingGroupKey]!
|
|
159
|
+
for (const label of activeGroup) {
|
|
160
|
+
graphics.rects.push({
|
|
161
|
+
center: label.center,
|
|
162
|
+
width: label.width,
|
|
163
|
+
height: label.height,
|
|
164
|
+
fill: "rgba(255, 165, 0, 0.5)", // Orange highlight
|
|
165
|
+
stroke: "orange",
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
for (const finalLabel of this.output.netLabelPlacements) {
|
|
147
171
|
const isMerged = finalLabel.globalConnNetId.startsWith("merged-group-")
|
|
148
172
|
const color = getColorFromString(finalLabel.globalConnNetId)
|
|
149
173
|
|
|
150
174
|
if (isMerged) {
|
|
151
|
-
// Draw the new merged label
|
|
152
175
|
graphics.rects.push({
|
|
153
176
|
center: finalLabel.center,
|
|
154
177
|
width: finalLabel.width,
|
|
155
178
|
height: finalLabel.height,
|
|
156
|
-
fill: color.replace(/, 1\)/, ", 0.2)"),
|
|
179
|
+
fill: color.replace(/, 1\)/, ", 0.2)"),
|
|
157
180
|
stroke: color,
|
|
158
181
|
label: finalLabel.globalConnNetId,
|
|
159
182
|
})
|
|
@@ -164,7 +187,6 @@ export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
|
164
187
|
for (const originalNetId of originalNetIds) {
|
|
165
188
|
const originalLabel = originalLabelsById.get(originalNetId)
|
|
166
189
|
if (originalLabel) {
|
|
167
|
-
// Draw the original label as a dashed box
|
|
168
190
|
const bounds = getRectBounds(
|
|
169
191
|
originalLabel.center,
|
|
170
192
|
originalLabel.width,
|
|
@@ -179,7 +201,6 @@ export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
|
179
201
|
strokeColor: color,
|
|
180
202
|
strokeDash: "4 4",
|
|
181
203
|
})
|
|
182
|
-
// Draw line from original to new center
|
|
183
204
|
graphics.lines.push({
|
|
184
205
|
points: [originalLabel.center, finalLabel.center],
|
|
185
206
|
strokeColor: color,
|
|
@@ -189,7 +210,6 @@ export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
|
189
210
|
}
|
|
190
211
|
}
|
|
191
212
|
} else {
|
|
192
|
-
// Draw un-merged labels
|
|
193
213
|
graphics.rects.push({
|
|
194
214
|
center: finalLabel.center,
|
|
195
215
|
width: finalLabel.width,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
2
|
+
import type { SolvedTracePath } from "../../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import type { Point } from "graphics-debug"
|
|
4
|
+
import { distance } from "@tscircuit/math-utils"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Filters a list of labels, returning only those that are physically located
|
|
8
|
+
* near the start or end point of any trace.
|
|
9
|
+
*
|
|
10
|
+
* This is hack so we do not need to know the position of ports
|
|
11
|
+
* and using only the trace geometry. we find out which labels are
|
|
12
|
+
* at/near port of chips and only keep those for merging.
|
|
13
|
+
*/
|
|
14
|
+
export const filterLabelsAtTraceEdges = ({
|
|
15
|
+
labels,
|
|
16
|
+
traces,
|
|
17
|
+
distanceThreshold = 0.5, // Example threshold
|
|
18
|
+
}: {
|
|
19
|
+
labels: NetLabelPlacement[]
|
|
20
|
+
traces: SolvedTracePath[]
|
|
21
|
+
distanceThreshold?: number
|
|
22
|
+
}): NetLabelPlacement[] => {
|
|
23
|
+
// 1. Group traces by their net ID for efficient lookup
|
|
24
|
+
const tracesByNetId = new Map<string, SolvedTracePath[]>()
|
|
25
|
+
if (!traces) {
|
|
26
|
+
throw new Error("No traces provided to filterLabelsAtTraceEdges")
|
|
27
|
+
}
|
|
28
|
+
for (const trace of traces) {
|
|
29
|
+
if (!trace.globalConnNetId) continue
|
|
30
|
+
if (!tracesByNetId.has(trace.globalConnNetId)) {
|
|
31
|
+
tracesByNetId.set(trace.globalConnNetId, [])
|
|
32
|
+
}
|
|
33
|
+
tracesByNetId.get(trace.globalConnNetId)!.push(trace)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const filteredLabels: NetLabelPlacement[] = []
|
|
37
|
+
|
|
38
|
+
// 2. Iterate through labels and check proximity against only relevant traces
|
|
39
|
+
for (const label of labels) {
|
|
40
|
+
// Check if it's a port-only label (no associated MSP connection pairs)
|
|
41
|
+
if (label.mspConnectionPairIds.length === 0) {
|
|
42
|
+
filteredLabels.push(label)
|
|
43
|
+
continue // Skip trace-edge checks for port-only labels
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const relevantTraces = tracesByNetId.get(label.globalConnNetId)
|
|
47
|
+
let isNearTraceEdge = false
|
|
48
|
+
|
|
49
|
+
if (!relevantTraces || relevantTraces.length === 0) {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const trace of relevantTraces) {
|
|
54
|
+
if (trace.tracePath.length === 0) continue
|
|
55
|
+
|
|
56
|
+
const startPoint = trace.tracePath[0]
|
|
57
|
+
const endPoint = trace.tracePath[trace.tracePath.length - 1]
|
|
58
|
+
|
|
59
|
+
const startDist = distance(label.center, startPoint)
|
|
60
|
+
const endDist = distance(label.center, endPoint)
|
|
61
|
+
|
|
62
|
+
if (startDist <= distanceThreshold || endDist <= distanceThreshold) {
|
|
63
|
+
isNearTraceEdge = true
|
|
64
|
+
break // Found a nearby edge, no need to check other traces for this label
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isNearTraceEdge) {
|
|
69
|
+
filteredLabels.push(label)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return filteredLabels
|
|
74
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
2
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Groups NetLabelPlacement objects by their associated chip ID and orientation.
|
|
6
|
+
* Labels are grouped if they belong to the same chip and are positioned
|
|
7
|
+
* with the same orientation relative to that chip.
|
|
8
|
+
*
|
|
9
|
+
* @param labels An array of NetLabelPlacement objects to be grouped.
|
|
10
|
+
* @param chips An array of Chip objects from the InputProblem. (Currently not directly used for grouping logic, but part of the signature).
|
|
11
|
+
* @returns A record where keys are in the format "chipId-orientation" (e.g., "U1-left")
|
|
12
|
+
* and values are arrays of NetLabelPlacement objects belonging to that group.
|
|
13
|
+
*/
|
|
14
|
+
export const groupLabelsByChipAndOrientation = ({
|
|
15
|
+
labels,
|
|
16
|
+
chips,
|
|
17
|
+
}: {
|
|
18
|
+
labels: NetLabelPlacement[]
|
|
19
|
+
chips: InputProblem["chips"]
|
|
20
|
+
}): Record<string, NetLabelPlacement[]> => {
|
|
21
|
+
const groupedLabels: Record<string, NetLabelPlacement[]> = {}
|
|
22
|
+
|
|
23
|
+
for (const label of labels) {
|
|
24
|
+
if (label.pinIds.length === 0) {
|
|
25
|
+
// Labels without pinIds cannot be associated with a chip and orientation for merging
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Extract chipId from the first pinId (e.g., "U1.1" -> "U1")
|
|
30
|
+
const chipId = label.pinIds[0].split(".")[0]
|
|
31
|
+
if (!chipId) {
|
|
32
|
+
// Should not happen if pinIds are well-formed, but good to guard
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const key = `${chipId}-${label.orientation}`
|
|
37
|
+
|
|
38
|
+
if (!groupedLabels[key]) {
|
|
39
|
+
groupedLabels[key] = []
|
|
40
|
+
}
|
|
41
|
+
groupedLabels[key].push(label)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return groupedLabels
|
|
45
|
+
}
|