@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.
Files changed (27) hide show
  1. package/.github/workflows/bun-formatcheck.yml +1 -1
  2. package/.github/workflows/bun-pver-release.yml +1 -1
  3. package/.github/workflows/bun-test.yml +1 -1
  4. package/.github/workflows/bun-typecheck.yml +1 -1
  5. package/dist/index.d.ts +35 -9
  6. package/dist/index.js +543 -186
  7. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +92 -50
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +26 -11
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +16 -5
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +95 -75
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts +74 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +45 -0
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/mergeLabelGroup.ts +71 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +162 -28
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/doesTraceStartOrEndInLabel.ts +58 -0
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/isPointInsideLabel.ts +23 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts +54 -0
  18. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +13 -1
  19. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +110 -60
  20. package/package.json +1 -1
  21. package/site/examples/example27.page.tsx +4 -0
  22. package/tests/assets/example27.json +168 -0
  23. package/tests/examples/__snapshots__/example03.snap.svg +67 -67
  24. package/tests/examples/__snapshots__/example16.snap.svg +3 -3
  25. package/tests/examples/__snapshots__/example30.snap.svg +258 -0
  26. package/tests/examples/example30.test.ts +12 -0
  27. 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
- * This solver is a pipeline that runs a series of sub-solvers to resolve
19
- * trace-label overlaps and clean up the resulting traces.
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
- // sub-solver instances
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.traces = solverInput.traces
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
- // If a sub-solver is active, step it and check for completion.
40
- if (this.activeSubSolver) {
41
- this.activeSubSolver.step()
42
-
43
- if (this.activeSubSolver.solved) {
44
- this.activeSubSolver = null
45
- this.pipelineStepIndex++
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
- // If no sub-solver is active, create the next one in the pipeline.
54
- switch (this.pipelineStepIndex) {
55
- case 0:
56
- this.labelMergingSolver = new MergedNetLabelObstacleSolver({
57
- netLabelPlacements: this.netLabelPlacements,
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: this.traces,
79
+ traces: [currentTargetTrace],
60
80
  })
61
- this.activeSubSolver = this.labelMergingSolver
62
- break
81
+ labelMerger.solve()
82
+ const mergingOutput = labelMerger.getOutput()
63
83
 
64
- case 1:
65
- this.overlapAvoidanceSolver = new OverlapAvoidanceStepSolver({
84
+ const subSolver = new OverlapAvoidanceStepSolver({
66
85
  inputProblem: this.inputProblem,
67
- traces: this.traces,
68
- netLabelPlacements:
69
- this.labelMergingSolver!.getOutput().netLabelPlacements,
70
- mergedLabelNetIdMap:
71
- this.labelMergingSolver!.getOutput().mergedLabelNetIdMap,
86
+ traces: [currentTargetTrace],
87
+ initialNetLabelPlacements: this.netLabelPlacements,
88
+ mergedNetLabelPlacements: mergingOutput.netLabelPlacements,
89
+ mergedLabelNetIdMap: mergingOutput.mergedLabelNetIdMap,
90
+ detourCounts: this.detourCounts,
72
91
  })
73
- this.activeSubSolver = this.overlapAvoidanceSolver
74
- break
75
-
76
- default:
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
- break
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.overlapAvoidanceSolver?.getOutput().allTraces ?? this.traces,
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
- const output = this.getOutput()
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
- for (const label of output.netLabelPlacements) {
111
- const color = getColorFromString(label.globalConnNetId, 0.3)
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
- 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
  }
@@ -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 - labelPadding,
33
- minY: labelBoundsRaw.minY - labelPadding,
34
- maxX: labelBoundsRaw.maxX + labelPadding,
35
- maxY: labelBoundsRaw.maxY + labelPadding,
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, 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
+ }