@tscircuit/schematic-trace-solver 0.0.45 → 0.0.46
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 +29 -5
- package/dist/index.js +355 -90
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +17 -3
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +26 -11
- 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 +150 -27
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/isPointInsideLabel.ts +23 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts +54 -0
- package/package.json +1 -1
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +8 -8
|
@@ -15,8 +15,21 @@ interface TraceLabelOverlapAvoidanceSolverInput {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* A pipeline solver responsible for resolving overlaps between schematic traces and net labels.
|
|
19
|
+
*
|
|
20
|
+
* This solver orchestrates a sequence of sub-solvers to achieve its goal:
|
|
21
|
+
* 1. **MergedNetLabelObstacleSolver**: This solver first merges labels that are
|
|
22
|
+
* close to each other to form larger "obstacle" groups. This simplifies the
|
|
23
|
+
* problem by reducing the number of individual obstacles the traces need to avoid.
|
|
24
|
+
* 2. **OverlapAvoidanceStepSolver**: This solver then takes the output of the merging
|
|
25
|
+
* step and iteratively attempts to reroute traces to avoid the merged label obstacles.
|
|
26
|
+
* It handles one overlap at a time, making it a step-by-step process.
|
|
27
|
+
*
|
|
28
|
+
* The final output is a set of modified traces that have been rerouted to avoid
|
|
29
|
+
* labels, and the set of merged labels that were used as obstacles.
|
|
30
|
+
*
|
|
31
|
+
* @param {TraceLabelOverlapAvoidanceSolverInput} solverInput - The input for the solver,
|
|
32
|
+
* containing the initial traces, label placements, and the input problem definition.
|
|
20
33
|
*/
|
|
21
34
|
export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
22
35
|
inputProblem: InputProblem
|
|
@@ -65,7 +78,8 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
|
65
78
|
this.overlapAvoidanceSolver = new OverlapAvoidanceStepSolver({
|
|
66
79
|
inputProblem: this.inputProblem,
|
|
67
80
|
traces: this.traces,
|
|
68
|
-
netLabelPlacements
|
|
81
|
+
initialNetLabelPlacements: this.netLabelPlacements, // The original, unfiltered list
|
|
82
|
+
mergedNetLabelPlacements:
|
|
69
83
|
this.labelMergingSolver!.getOutput().netLabelPlacements,
|
|
70
84
|
mergedLabelNetIdMap:
|
|
71
85
|
this.labelMergingSolver!.getOutput().mergedLabelNetIdMap,
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
2
|
+
import { getRectBounds } from "../../../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
3
|
+
import type { Point } from "graphics-debug" // Assuming Point is from graphics-debug or similar
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merges a group of NetLabelPlacement objects into a single, larger NetLabelPlacement.
|
|
7
|
+
* It calculates a new bounding box that encompasses all labels in the group,
|
|
8
|
+
* aggregates unique pin and MSP connection pair IDs, and creates a synthetic
|
|
9
|
+
* merged label.
|
|
10
|
+
*
|
|
11
|
+
* @param group An array of NetLabelPlacement objects to be merged. Assumes the group is not empty.
|
|
12
|
+
* @param groupKey A string key representing the group (e.g., "chip1-left"), used for generating the merged label's globalConnNetId.
|
|
13
|
+
* @returns An object containing the newly created merged NetLabelPlacement and a Set of the original globalConnNetIds that were merged.
|
|
14
|
+
* @throws Error if the input group is empty.
|
|
15
|
+
*/
|
|
16
|
+
export const mergeLabelGroup = (
|
|
17
|
+
group: NetLabelPlacement[],
|
|
18
|
+
groupKey: string,
|
|
19
|
+
): {
|
|
20
|
+
mergedLabel: NetLabelPlacement
|
|
21
|
+
originalNetIds: Set<string>
|
|
22
|
+
} => {
|
|
23
|
+
if (group.length === 0) {
|
|
24
|
+
throw new Error("Cannot merge an empty group of labels.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let minX = Infinity
|
|
28
|
+
let minY = Infinity
|
|
29
|
+
let maxX = -Infinity
|
|
30
|
+
let maxY = -Infinity
|
|
31
|
+
|
|
32
|
+
const allPinIds = new Set<string>()
|
|
33
|
+
const allMspConnectionPairIds = new Set<string>()
|
|
34
|
+
const originalNetIds = new Set<string>()
|
|
35
|
+
|
|
36
|
+
for (const label of group) {
|
|
37
|
+
const bounds = getRectBounds(label.center, label.width, label.height)
|
|
38
|
+
minX = Math.min(minX, bounds.minX)
|
|
39
|
+
minY = Math.min(minY, bounds.minY)
|
|
40
|
+
maxX = Math.max(maxX, bounds.maxX)
|
|
41
|
+
maxY = Math.max(maxY, bounds.maxY)
|
|
42
|
+
|
|
43
|
+
label.pinIds.forEach((id) => allPinIds.add(id))
|
|
44
|
+
label.mspConnectionPairIds.forEach((id) => allMspConnectionPairIds.add(id))
|
|
45
|
+
originalNetIds.add(label.globalConnNetId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const newWidth = maxX - minX
|
|
49
|
+
const newHeight = maxY - minY
|
|
50
|
+
const newCenter: Point = { x: minX + newWidth / 2, y: minY + newHeight / 2 }
|
|
51
|
+
|
|
52
|
+
// Use the first label as a template for properties that are consistent across the group
|
|
53
|
+
// like orientation.
|
|
54
|
+
const template = group[0]!
|
|
55
|
+
const syntheticId = `merged-group-${groupKey}`
|
|
56
|
+
|
|
57
|
+
const mergedLabel: NetLabelPlacement = {
|
|
58
|
+
...template, // Copy common properties
|
|
59
|
+
globalConnNetId: syntheticId,
|
|
60
|
+
center: newCenter,
|
|
61
|
+
width: newWidth,
|
|
62
|
+
height: newHeight,
|
|
63
|
+
pinIds: Array.from(allPinIds),
|
|
64
|
+
mspConnectionPairIds: Array.from(allMspConnectionPairIds),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
mergedLabel,
|
|
69
|
+
originalNetIds,
|
|
70
|
+
}
|
|
71
|
+
}
|