@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.
@@ -15,8 +15,21 @@ interface TraceLabelOverlapAvoidanceSolverInput {
15
15
  }
16
16
 
17
17
  /**
18
- * This solver is a pipeline that runs a series of sub-solvers to resolve
19
- * trace-label overlaps and clean up the resulting traces.
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
- export const detectTraceLabelOverlap = (
12
- traces: SolvedTracePath[],
13
- netLabels: NetLabelPlacement[],
14
- ): TraceLabelOverlap[] => {
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 i = 0; i < trace.tracePath.length - 1; i++) {
22
- const p1 = trace.tracePath[i]
23
- const p2 = trace.tracePath[i + 1]
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
- // Check if the trace and label belong to the same net.
27
- // If so, it's a self-attachment, not a collision.
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 // Move to the next label
44
+ break // Break from the inner-most loop (segment loop)
30
45
  }
31
46
  overlaps.push({ trace, label })
32
- break // Move to the next label
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, reducing schematic clutter.
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
- constructor(solverInput: LabelMergingSolverInput) {
32
- // console.log(JSON.stringify(solverInput));
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
- const originalLabels = this.input.netLabelPlacements
48
- const mergedLabelNetIdMap: Record<string, Set<string>> = {}
49
-
50
- if (!originalLabels || originalLabels.length === 0) {
51
- this.output = {
52
- netLabelPlacements: [],
53
- mergedLabelNetIdMap: {},
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
- const newWidth = maxX - minX
92
- const newHeight = maxY - minY
93
- const template = group[0]!
94
- const syntheticId = `merged-group-${key}`
95
- const originalNetIds = new Set(group.map((p) => p.globalConnNetId))
96
- mergedLabelNetIdMap[syntheticId] = originalNetIds
97
-
98
- finalPlacements.push({
99
- ...template,
100
- globalConnNetId: syntheticId,
101
- width: newWidth,
102
- height: newHeight,
103
- center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
104
- pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
105
- mspConnectionPairIds: [
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
- this.output = {
112
- netLabelPlacements: finalPlacements,
113
- mergedLabelNetIdMap,
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)"), // semi-transparent
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
+ }