@tscircuit/schematic-trace-solver 0.0.44 → 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.
Files changed (39) 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 +43 -8
  6. package/dist/index.js +1109 -185
  7. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +0 -1
  8. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +83 -36
  9. package/lib/solvers/TraceCleanupSolver/hasCollisions.ts +16 -4
  10. package/lib/solvers/TraceCleanupSolver/is4PointRectangle.ts +17 -0
  11. package/lib/solvers/TraceCleanupSolver/isSegmentAnEndpointSegment.ts +36 -0
  12. package/lib/solvers/TraceCleanupSolver/mergeGraphicsObjects.ts +28 -0
  13. package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +18 -1
  14. package/lib/solvers/TraceCleanupSolver/recognizeStairStepPattern.ts +56 -0
  15. package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +370 -0
  16. package/lib/solvers/TraceCleanupSolver/sub-solver/findAllLShapedTurns.ts +48 -0
  17. package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +36 -0
  18. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +107 -0
  19. package/lib/solvers/TraceCleanupSolver/sub-solver/generateRectangleCandidates.ts +55 -0
  20. package/lib/solvers/TraceCleanupSolver/sub-solver/getTraceObstacles.ts +25 -0
  21. package/lib/solvers/TraceCleanupSolver/sub-solver/isPathColliding.ts +58 -0
  22. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCandidates.ts +33 -0
  23. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCollision.ts +20 -0
  24. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeIntersectionPoints.ts +26 -0
  25. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeLSapes.ts +33 -0
  26. package/lib/solvers/TraceCleanupSolver/turnMinimization.ts +50 -56
  27. package/lib/solvers/TraceCleanupSolver/visualizeTightRectangle.ts +24 -0
  28. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +17 -3
  29. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +26 -11
  30. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +95 -75
  31. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/filterLabelsAtTraceEdges.ts +74 -0
  32. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +45 -0
  33. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/mergeLabelGroup.ts +71 -0
  34. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +150 -27
  35. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/isPointInsideLabel.ts +23 -0
  36. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/visualizeDecomposition.ts +54 -0
  37. package/package.json +1 -1
  38. package/tests/examples/__snapshots__/example29.snap.svg +6 -6
  39. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +8 -8
@@ -0,0 +1,33 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import type { Point } from "@tscircuit/math-utils"
3
+
4
+ /**
5
+ * Visualizes a set of candidate paths and optional intersection points.
6
+ * It draws each candidate path as a line with a specified color and marks intersection points with green circles.
7
+ * This function is useful for debugging and understanding the rerouting process.
8
+ */
9
+ export const visualizeCandidates = (
10
+ candidates: Point[][],
11
+ color = "gray",
12
+ intersectionPoints: Point[] = [],
13
+ ): GraphicsObject => {
14
+ const graphics: GraphicsObject = { lines: [], circles: [] }
15
+
16
+ for (const candidate of candidates) {
17
+ graphics.lines!.push({
18
+ points: candidate,
19
+ strokeColor: color,
20
+ })
21
+ }
22
+
23
+ // Draw intersection points
24
+ for (const point of intersectionPoints) {
25
+ graphics.circles!.push({
26
+ center: point,
27
+ radius: 0.01, // Larger radius for intersection points
28
+ fill: "green",
29
+ })
30
+ }
31
+
32
+ return graphics
33
+ }
@@ -0,0 +1,20 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import type { CollisionInfo } from "./isPathColliding"
3
+
4
+ /**
5
+ * Visualizes a collision point if collision information is provided and a collision occurred.
6
+ * It draws a red circle at the collision point to highlight the location of the collision.
7
+ */
8
+ export const visualizeCollision = (
9
+ collisionInfo: CollisionInfo | null,
10
+ ): GraphicsObject => {
11
+ const collisionGraphics: GraphicsObject = { circles: [] }
12
+ if (collisionInfo?.isColliding && collisionInfo.collisionPoint) {
13
+ collisionGraphics.circles!.push({
14
+ center: collisionInfo.collisionPoint,
15
+ radius: 0.01,
16
+ fill: "red",
17
+ })
18
+ }
19
+ return collisionGraphics
20
+ }
@@ -0,0 +1,26 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+
4
+ /**
5
+ * Visualizes a set of intersection points by drawing circles at their locations.
6
+ * This function is used to highlight where different trace segments or obstacles intersect.
7
+ */
8
+ export const visualizeIntersectionPoints = (
9
+ points: Point[],
10
+ color = "red",
11
+ ): GraphicsObject => {
12
+ const graphics: GraphicsObject = { circles: [] }
13
+
14
+ for (const point of points) {
15
+ graphics.circles!.push({
16
+ center: {
17
+ x: point.x,
18
+ y: point.y,
19
+ },
20
+ radius: 0.01,
21
+ fill: color,
22
+ })
23
+ }
24
+
25
+ return graphics
26
+ }
@@ -0,0 +1,33 @@
1
+ import type { LShape } from "./findAllLShapedTurns"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+
4
+ /**
5
+ * Visualizes L-shaped turns by drawing a blue circle at the corner point (p2)
6
+ * and light blue lines connecting p1, p2, and p3.
7
+ * This function can visualize a single L-shape or an array of L-shapes.
8
+ */
9
+ export const visualizeLSapes = (lShapes: LShape[] | LShape): GraphicsObject => {
10
+ const graphics: GraphicsObject = { circles: [], lines: [] }
11
+
12
+ const lShapesArray = Array.isArray(lShapes) ? lShapes : [lShapes]
13
+
14
+ for (const lShape of lShapesArray) {
15
+ // Draw the center point as a blue ball
16
+ graphics.circles!.push({
17
+ center: {
18
+ x: lShape.p2.x,
19
+ y: lShape.p2.y,
20
+ },
21
+ radius: 0.01,
22
+ fill: "blue",
23
+ })
24
+
25
+ // Draw the two lines in a light blue color
26
+ graphics.lines!.push({
27
+ points: [lShape.p1, lShape.p2, lShape.p3],
28
+ strokeColor: "lightblue",
29
+ })
30
+ }
31
+
32
+ return graphics
33
+ }
@@ -4,75 +4,32 @@ import { countTurns } from "./countTurns"
4
4
  import { simplifyPath } from "./simplifyPath"
5
5
  import { tryConnectPoints } from "./tryConnectPoints"
6
6
  import { hasCollisionsWithLabels } from "./hasCollisionsWithLabels"
7
-
7
+ import { recognizeStairStepPattern } from "./recognizeStairStepPattern"
8
+ import { isSegmentAnEndpointSegment } from "./isSegmentAnEndpointSegment"
9
+
10
+ /**
11
+ * Minimizes the number of turns in a given path while avoiding collisions with obstacles and labels.
12
+ * This function employs an iterative approach, attempting to simplify the path in several ways:
13
+ * 1. **Stair-step pattern recognition**: It first looks for and attempts to simplify stair-step patterns by connecting the start and end points of the pattern with a simpler path, if no collisions are introduced.
14
+ * 2. **Point removal and reconnection**: If no stair-step optimization is possible, it tries to remove intermediate points and reconnect the remaining segments with a simpler path, prioritizing solutions that reduce turns or path length.
15
+ * 3. **Collinear segment merging**: Finally, it attempts to merge collinear segments to further simplify the path.
16
+ * The process continues until no further improvements can be made without introducing collisions.
17
+ */
8
18
  export const minimizeTurns = ({
9
19
  path,
10
20
  obstacles,
11
21
  labelBounds,
22
+ originalPath,
12
23
  }: {
13
24
  path: Point[]
14
25
  obstacles: any[]
15
26
  labelBounds: any[]
27
+ originalPath: Point[]
16
28
  }): Point[] => {
17
29
  if (path.length <= 2) {
18
30
  return path
19
31
  }
20
32
 
21
- const recognizeStairStepPattern = (
22
- pathToCheck: Point[],
23
- startIdx: number,
24
- ): number => {
25
- if (startIdx >= pathToCheck.length - 3) return -1
26
-
27
- let endIdx = startIdx
28
- let isStairStep = true
29
-
30
- for (
31
- let i = startIdx;
32
- i < pathToCheck.length - 2 && i < startIdx + 10;
33
- i++
34
- ) {
35
- if (i + 2 >= pathToCheck.length) break
36
-
37
- const p1 = pathToCheck[i]
38
- const p2 = pathToCheck[i + 1]
39
- const p3 = pathToCheck[i + 2]
40
-
41
- const seg1Vertical = p1.x === p2.x
42
- const seg2Vertical = p2.x === p3.x
43
-
44
- if (seg1Vertical === seg2Vertical) {
45
- break
46
- }
47
-
48
- const seg1Direction = seg1Vertical
49
- ? Math.sign(p2.y - p1.y)
50
- : Math.sign(p2.x - p1.x)
51
-
52
- if (i > startIdx) {
53
- const prevP = pathToCheck[i - 1]
54
- const prevSegVertical = prevP.x === p1.x
55
- const prevDirection = prevSegVertical
56
- ? Math.sign(p1.y - prevP.y)
57
- : Math.sign(p1.x - prevP.x)
58
-
59
- if (
60
- (seg1Vertical &&
61
- prevSegVertical &&
62
- seg1Direction !== prevDirection) ||
63
- (!seg1Vertical && !prevSegVertical && seg1Direction !== prevDirection)
64
- ) {
65
- isStairStep = false
66
- break
67
- }
68
- }
69
-
70
- endIdx = i + 2
71
- }
72
-
73
- return isStairStep && endIdx - startIdx >= 3 ? endIdx : -1
74
- }
75
-
76
33
  let optimizedPath = [...path]
77
34
  let currentTurns = countTurns(optimizedPath)
78
35
  let improved = true
@@ -85,6 +42,21 @@ export const minimizeTurns = ({
85
42
  const stairEndIdx = recognizeStairStepPattern(optimizedPath, startIdx)
86
43
 
87
44
  if (stairEndIdx > 0) {
45
+ if (
46
+ isSegmentAnEndpointSegment(
47
+ optimizedPath[startIdx],
48
+ optimizedPath[startIdx + 1],
49
+ originalPath,
50
+ ) ||
51
+ isSegmentAnEndpointSegment(
52
+ optimizedPath[stairEndIdx - 1],
53
+ optimizedPath[stairEndIdx],
54
+ originalPath,
55
+ )
56
+ ) {
57
+ continue
58
+ }
59
+
88
60
  const startPoint = optimizedPath[startIdx]
89
61
  const endPoint = optimizedPath[stairEndIdx]
90
62
 
@@ -129,6 +101,21 @@ export const minimizeTurns = ({
129
101
 
130
102
  if (endIdx >= optimizedPath.length) continue
131
103
 
104
+ if (
105
+ isSegmentAnEndpointSegment(
106
+ optimizedPath[startIdx],
107
+ optimizedPath[startIdx + 1],
108
+ originalPath,
109
+ ) ||
110
+ isSegmentAnEndpointSegment(
111
+ optimizedPath[endIdx - 1],
112
+ optimizedPath[endIdx],
113
+ originalPath,
114
+ )
115
+ ) {
116
+ continue
117
+ }
118
+
132
119
  const startPoint = optimizedPath[startIdx]
133
120
  const endPoint = optimizedPath[endIdx]
134
121
 
@@ -179,6 +166,13 @@ export const minimizeTurns = ({
179
166
  const p2 = optimizedPath[i + 1]
180
167
  const p3 = optimizedPath[i + 2]
181
168
 
169
+ if (
170
+ isSegmentAnEndpointSegment(p1, p2, originalPath) ||
171
+ isSegmentAnEndpointSegment(p2, p3, originalPath)
172
+ ) {
173
+ continue
174
+ }
175
+
182
176
  const allVertical = p1.x === p2.x && p2.x === p3.x
183
177
  const allHorizontal = p1.y === p2.y && p2.y === p3.y
184
178
 
@@ -0,0 +1,24 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import type { Rectangle } from "./sub-solver/generateRectangleCandidates"
3
+
4
+ /**
5
+ * Visualizes a given rectangle by drawing it as a green-stroked rectangle.
6
+ * This function is useful for highlighting specific rectangular areas in a graphical representation.
7
+ */
8
+ export const visualizeTightRectangle = (
9
+ rectangle: Rectangle,
10
+ ): GraphicsObject => {
11
+ const graphics: GraphicsObject = { rects: [] }
12
+
13
+ graphics.rects!.push({
14
+ center: {
15
+ x: rectangle.x + rectangle.width / 2,
16
+ y: rectangle.y + rectangle.height / 2,
17
+ },
18
+ width: rectangle.width,
19
+ height: rectangle.height,
20
+ stroke: "green",
21
+ })
22
+
23
+ return graphics
24
+ }
@@ -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,