@tscircuit/schematic-trace-solver 0.0.44 → 0.0.45

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 (25) hide show
  1. package/dist/index.d.ts +14 -3
  2. package/dist/index.js +754 -95
  3. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +0 -1
  4. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +83 -36
  5. package/lib/solvers/TraceCleanupSolver/hasCollisions.ts +16 -4
  6. package/lib/solvers/TraceCleanupSolver/is4PointRectangle.ts +17 -0
  7. package/lib/solvers/TraceCleanupSolver/isSegmentAnEndpointSegment.ts +36 -0
  8. package/lib/solvers/TraceCleanupSolver/mergeGraphicsObjects.ts +28 -0
  9. package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +18 -1
  10. package/lib/solvers/TraceCleanupSolver/recognizeStairStepPattern.ts +56 -0
  11. package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +370 -0
  12. package/lib/solvers/TraceCleanupSolver/sub-solver/findAllLShapedTurns.ts +48 -0
  13. package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +36 -0
  14. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +107 -0
  15. package/lib/solvers/TraceCleanupSolver/sub-solver/generateRectangleCandidates.ts +55 -0
  16. package/lib/solvers/TraceCleanupSolver/sub-solver/getTraceObstacles.ts +25 -0
  17. package/lib/solvers/TraceCleanupSolver/sub-solver/isPathColliding.ts +58 -0
  18. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCandidates.ts +33 -0
  19. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeCollision.ts +20 -0
  20. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeIntersectionPoints.ts +26 -0
  21. package/lib/solvers/TraceCleanupSolver/sub-solver/visualizeLSapes.ts +33 -0
  22. package/lib/solvers/TraceCleanupSolver/turnMinimization.ts +50 -56
  23. package/lib/solvers/TraceCleanupSolver/visualizeTightRectangle.ts +24 -0
  24. package/package.json +1 -1
  25. package/tests/examples/__snapshots__/example29.snap.svg +6 -6
@@ -200,7 +200,6 @@ export class SchematicTracePipelineSolver extends BaseSolver {
200
200
  {
201
201
  inputProblem: instance.inputProblem,
202
202
  allTraces: traces,
203
- targetTraceIds: new Set(traces.map((t) => t.mspPairId)),
204
203
  allLabelPlacements: labelMergingOutput.netLabelPlacements,
205
204
  mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
206
205
  paddingBuffer: 0.1,
@@ -7,74 +7,120 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
7
7
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
8
  import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
9
9
 
10
+ /**
11
+ * Defines the input structure for the TraceCleanupSolver.
12
+ */
10
13
  interface TraceCleanupSolverInput {
11
14
  inputProblem: InputProblem
12
15
  allTraces: SolvedTracePath[]
13
- targetTraceIds: Set<string>
14
16
  allLabelPlacements: NetLabelPlacement[]
15
17
  mergedLabelNetIdMap: Record<string, Set<string>>
16
18
  paddingBuffer: number
17
19
  }
18
20
 
19
- type PipelineStep = "minimizing_turns" | "balancing_l_shapes"
21
+ import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
22
+ import { is4PointRectangle } from "./is4PointRectangle"
20
23
 
21
24
  /**
22
- * Cleans up traces by minimizing turns and balancing L-shapes to improve
23
- * the overall aesthetics and readability of the schematic.
25
+ * Represents the different stages or steps within the trace cleanup pipeline.
26
+ */
27
+ type PipelineStep =
28
+ | "minimizing_turns"
29
+ | "balancing_l_shapes"
30
+ | "untangling_traces"
31
+
32
+ /**
33
+ * The TraceCleanupSolver is responsible for improving the aesthetics and readability of schematic traces.
34
+ * It operates in a multi-step pipeline:
35
+ * 1. **Untangling Traces**: It first attempts to untangle any overlapping or highly convoluted traces using a sub-solver.
36
+ * 2. **Minimizing Turns**: After untangling, it iterates through each trace to minimize the number of turns, simplifying their paths.
37
+ * 3. **Balancing L-Shapes**: Finally, it balances L-shaped trace segments to create more visually appealing and consistent layouts.
38
+ * The solver processes traces one by one, applying these cleanup steps sequentially to refine the overall trace layout.
24
39
  */
25
40
  export class TraceCleanupSolver extends BaseSolver {
26
41
  private input: TraceCleanupSolverInput
27
42
  private outputTraces: SolvedTracePath[]
28
43
  private traceIdQueue: string[]
29
44
  private tracesMap: Map<string, SolvedTracePath>
30
- private pipelineStep: PipelineStep = "minimizing_turns"
45
+ private pipelineStep: PipelineStep = "untangling_traces"
31
46
  private activeTraceId: string | null = null // New property
47
+ override activeSubSolver: BaseSolver | null = null
32
48
 
33
49
  constructor(solverInput: TraceCleanupSolverInput) {
34
50
  super()
35
51
  this.input = solverInput
36
52
  this.outputTraces = [...solverInput.allTraces]
37
53
  this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
38
- this.traceIdQueue = Array.from(solverInput.targetTraceIds)
54
+ this.traceIdQueue = Array.from(
55
+ solverInput.allTraces.map((e) => e.mspPairId),
56
+ )
39
57
  }
40
58
 
41
59
  override _step() {
42
- if (
43
- this.pipelineStep === "minimizing_turns" &&
44
- this.traceIdQueue.length === 0
45
- ) {
60
+ if (this.activeSubSolver) {
61
+ this.activeSubSolver.step()
62
+ if (this.activeSubSolver.solved) {
63
+ const output = (
64
+ this.activeSubSolver as UntangleTraceSubsolver
65
+ ).getOutput()
66
+ this.outputTraces = output.traces
67
+ this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
68
+ this.activeSubSolver = null
69
+ this.pipelineStep = "minimizing_turns"
70
+ } else if (this.activeSubSolver.failed) {
71
+ this.activeSubSolver = null
72
+ this.pipelineStep = "minimizing_turns"
73
+ }
74
+ return
75
+ }
76
+
77
+ switch (this.pipelineStep) {
78
+ case "untangling_traces":
79
+ this._runUntangleTracesStep()
80
+ break
81
+ case "minimizing_turns":
82
+ this._runMinimizeTurnsStep()
83
+ break
84
+ case "balancing_l_shapes":
85
+ this._runBalanceLShapesStep()
86
+ break
87
+ }
88
+ }
89
+
90
+ private _runUntangleTracesStep() {
91
+ this.activeSubSolver = new UntangleTraceSubsolver({
92
+ ...this.input,
93
+ allTraces: Array.from(this.tracesMap.values()),
94
+ })
95
+ }
96
+
97
+ private _runMinimizeTurnsStep() {
98
+ if (this.traceIdQueue.length === 0) {
46
99
  this.pipelineStep = "balancing_l_shapes"
47
- this.traceIdQueue = Array.from(this.input.targetTraceIds)
100
+ this.traceIdQueue = Array.from(
101
+ this.input.allTraces.map((e) => e.mspPairId),
102
+ )
103
+ return
48
104
  }
49
105
 
50
- if (
51
- this.pipelineStep === "balancing_l_shapes" &&
52
- this.traceIdQueue.length === 0
53
- ) {
106
+ this._processTrace("minimizing_turns")
107
+ }
108
+
109
+ private _runBalanceLShapesStep() {
110
+ if (this.traceIdQueue.length === 0) {
54
111
  this.solved = true
55
112
  return
56
113
  }
57
114
 
115
+ this._processTrace("balancing_l_shapes")
116
+ }
117
+
118
+ private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") {
58
119
  const targetMspConnectionPairId = this.traceIdQueue.shift()!
59
- this.activeTraceId = targetMspConnectionPairId // Set active trace ID
120
+ this.activeTraceId = targetMspConnectionPairId
60
121
  const originalTrace = this.tracesMap.get(targetMspConnectionPairId)!
61
122
 
62
- const { tracePath } = originalTrace
63
-
64
- // Skip if the trace is a perfect rectangle
65
- const is4PointRectangle = (path: typeof tracePath): boolean => {
66
- if (path.length !== 4) return false
67
- const [p0, p1, p2, p3] = path
68
- // H-V-H "C" shape
69
- const isHVHC =
70
- p0.y === p1.y && p1.x === p2.x && p2.y === p3.y && p0.x === p3.x
71
- // V-H-V "C" shape
72
- const isVHVC =
73
- p0.x === p1.x && p1.y === p2.y && p2.x === p3.x && p0.y === p3.y
74
- return isHVHC || isVHVC
75
- }
76
-
77
- if (is4PointRectangle(tracePath)) {
123
+ if (is4PointRectangle(originalTrace.tracePath)) {
78
124
  return
79
125
  }
80
126
 
@@ -82,7 +128,7 @@ export class TraceCleanupSolver extends BaseSolver {
82
128
 
83
129
  let updatedTrace: SolvedTracePath
84
130
 
85
- if (this.pipelineStep === "minimizing_turns") {
131
+ if (step === "minimizing_turns") {
86
132
  updatedTrace = minimizeTurnsWithFilteredLabels({
87
133
  ...this.input,
88
134
  targetMspConnectionPairId,
@@ -96,10 +142,7 @@ export class TraceCleanupSolver extends BaseSolver {
96
142
  })
97
143
  }
98
144
 
99
- // Update the state in the central map
100
145
  this.tracesMap.set(targetMspConnectionPairId, updatedTrace)
101
-
102
- // Update the output for visualization
103
146
  this.outputTraces = Array.from(this.tracesMap.values())
104
147
  }
105
148
 
@@ -110,6 +153,10 @@ export class TraceCleanupSolver extends BaseSolver {
110
153
  }
111
154
 
112
155
  override visualize(): GraphicsObject {
156
+ if (this.activeSubSolver) {
157
+ return this.activeSubSolver.visualize()
158
+ }
159
+
113
160
  const graphics = visualizeInputProblem(this.input.inputProblem, {
114
161
  chipAlpha: 0.1,
115
162
  connectionAlpha: 0.1,
@@ -1,9 +1,13 @@
1
- import type { Point } from "graphics-debug"
2
- import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { segmentToBoxMinDistance } from "@tscircuit/math-utils"
3
3
 
4
+ /**
5
+ * Checks if a given path (series of segments) collides with any of the provided obstacles.
6
+ * It iterates through each segment of the path and checks for intersection with each obstacle.
7
+ */
4
8
  export const hasCollisions = (
5
9
  pathSegments: Point[],
6
- obstacles: any[],
10
+ obstacles: Array<{ minX: number; maxX: number; minY: number; maxY: number }>,
7
11
  ): boolean => {
8
12
  // Check each segment of the path
9
13
  for (let i = 0; i < pathSegments.length - 1; i++) {
@@ -12,7 +16,15 @@ export const hasCollisions = (
12
16
 
13
17
  // Check collision with each obstacle
14
18
  for (const obstacle of obstacles) {
15
- if (segmentIntersectsRect(p1, p2, obstacle)) {
19
+ const box = {
20
+ center: {
21
+ x: obstacle.minX + (obstacle.maxX - obstacle.minX) / 2,
22
+ y: obstacle.minY + (obstacle.maxY - obstacle.minY) / 2,
23
+ },
24
+ width: obstacle.maxX - obstacle.minX,
25
+ height: obstacle.maxY - obstacle.minY,
26
+ }
27
+ if (segmentToBoxMinDistance(p1, p2, box) <= 0) {
16
28
  return true
17
29
  }
18
30
  }
@@ -0,0 +1,17 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+
3
+ /**
4
+ * Checks if a given path of four points forms a rectangle with horizontal and vertical segments.
5
+ * It verifies if the path forms either an H-V-H "C" shape or a V-H-V "C" shape.
6
+ */
7
+ export const is4PointRectangle = (path: Point[]): boolean => {
8
+ if (path.length !== 4) return false
9
+ const [p0, p1, p2, p3] = path
10
+ // H-V-H "C" shape
11
+ const isHVHC =
12
+ p0.y === p1.y && p1.x === p2.x && p2.y === p3.y && p0.x === p3.x
13
+ // V-H-V "C" shape
14
+ const isVHVC =
15
+ p0.x === p1.x && p1.y === p2.y && p2.x === p3.x && p0.y === p3.y
16
+ return isHVHC || isVHVC
17
+ }
@@ -0,0 +1,36 @@
1
+ import type { Point } from "graphics-debug"
2
+
3
+ /**
4
+ * Determines if a given segment (p1-p2) is either the first or the last segment of an original path.
5
+ * This is useful for identifying segments that are at the extremities of a trace.
6
+ */
7
+ export const isSegmentAnEndpointSegment = (
8
+ p1: Point,
9
+ p2: Point,
10
+ originalPath: Point[],
11
+ ): boolean => {
12
+ if (originalPath.length < 2) return false
13
+
14
+ const originalStart = originalPath[0]
15
+ const originalEnd = originalPath[originalPath.length - 1]
16
+
17
+ // Check if p1-p2 is the first segment of the original path
18
+ if (
19
+ p1.x === originalStart.x &&
20
+ p1.y === originalStart.y &&
21
+ p2.x === originalPath[1].x &&
22
+ p2.y === originalPath[1].y
23
+ ) {
24
+ return true
25
+ }
26
+ // Check if p1-p2 is the last segment of the original path
27
+ if (
28
+ p1.x === originalPath[originalPath.length - 2].x &&
29
+ p1.y === originalPath[originalPath.length - 2].y &&
30
+ p2.x === originalEnd.x &&
31
+ p2.y === originalEnd.y
32
+ ) {
33
+ return true
34
+ }
35
+ return false
36
+ }
@@ -0,0 +1,28 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+
3
+ /**
4
+ * Merges multiple GraphicsObject instances into a single GraphicsObject.
5
+ * It combines all lines, points, rectangles, circles, and texts from the input objects.
6
+ */
7
+ export const mergeGraphicsObjects = (
8
+ objects: (GraphicsObject | undefined)[],
9
+ ): GraphicsObject => {
10
+ const merged: GraphicsObject = {
11
+ lines: [],
12
+ points: [],
13
+ rects: [],
14
+ circles: [],
15
+ texts: [],
16
+ }
17
+
18
+ for (const obj of objects) {
19
+ if (!obj) continue
20
+ if (obj.lines) merged.lines!.push(...obj.lines)
21
+ if (obj.points) merged.points!.push(...obj.points)
22
+ if (obj.rects) merged.rects!.push(...obj.rects)
23
+ if (obj.circles) merged.circles!.push(...obj.circles)
24
+ if (obj.texts) merged.texts!.push(...obj.texts)
25
+ }
26
+
27
+ return merged
28
+ }
@@ -4,6 +4,13 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
4
4
  import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
5
  import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
6
6
 
7
+ /**
8
+ * Minimizes the turns of a target trace while considering other traces and labels as obstacles.
9
+ * This function first identifies the target trace and separates it from other traces, which are then treated as obstacles.
10
+ * It also filters out labels that belong to the target trace's net, so they don't act as obstacles.
11
+ * The function then combines static obstacles (from the input problem) with the other traces and filtered labels to create a comprehensive set of obstacles.
12
+ * Finally, it uses a turn minimization algorithm to find a new path for the target trace that avoids these combined obstacles.
13
+ */
7
14
  export const minimizeTurnsWithFilteredLabels = ({
8
15
  targetMspConnectionPairId,
9
16
  traces,
@@ -44,7 +51,16 @@ export const minimizeTurnsWithFilteredLabels = ({
44
51
  }),
45
52
  )
46
53
 
47
- const staticObstacles = getObstacleRects(inputProblem)
54
+ const staticObstaclesRaw = getObstacleRects(inputProblem)
55
+ const PADDING = 0.01
56
+ const staticObstacles = staticObstaclesRaw.map((obs) => ({
57
+ ...obs,
58
+ minX: obs.minX - PADDING,
59
+ minY: obs.minY - PADDING,
60
+ maxX: obs.maxX + PADDING,
61
+ maxY: obs.maxY + PADDING,
62
+ }))
63
+
48
64
  const combinedObstacles = [...staticObstacles, ...traceObstacles]
49
65
 
50
66
  const originalPath = targetTrace.tracePath
@@ -67,6 +83,7 @@ export const minimizeTurnsWithFilteredLabels = ({
67
83
  path: originalPath,
68
84
  obstacles: combinedObstacles,
69
85
  labelBounds,
86
+ originalPath: originalPath,
70
87
  })
71
88
 
72
89
  return {
@@ -0,0 +1,56 @@
1
+ import type { Point } from "graphics-debug"
2
+
3
+ /**
4
+ * Recognizes a "stair-step" pattern within a given path of points starting from a specified index.
5
+ * A stair-step pattern is characterized by alternating horizontal and vertical segments.
6
+ * The function checks for a sequence of at least three segments where the orientation (horizontal/vertical) alternates.
7
+ * It returns the end index of the recognized stair-step pattern if found, otherwise -1.
8
+ */
9
+ export const recognizeStairStepPattern = (
10
+ pathToCheck: Point[],
11
+ startIdx: number,
12
+ ): number => {
13
+ if (startIdx >= pathToCheck.length - 3) return -1
14
+
15
+ let endIdx = startIdx
16
+ let isStairStep = true
17
+
18
+ for (let i = startIdx; i < pathToCheck.length - 2 && i < startIdx + 10; i++) {
19
+ if (i + 2 >= pathToCheck.length) break
20
+
21
+ const p1 = pathToCheck[i]
22
+ const p2 = pathToCheck[i + 1]
23
+ const p3 = pathToCheck[i + 2]
24
+
25
+ const seg1Vertical = p1.x === p2.x
26
+ const seg2Vertical = p2.x === p3.x
27
+
28
+ if (seg1Vertical === seg2Vertical) {
29
+ break
30
+ }
31
+
32
+ const seg1Direction = seg1Vertical
33
+ ? Math.sign(p2.y - p1.y)
34
+ : Math.sign(p2.x - p1.x)
35
+
36
+ if (i > startIdx) {
37
+ const prevP = pathToCheck[i - 1]
38
+ const prevSegVertical = prevP.x === p1.x
39
+ const prevDirection = prevSegVertical
40
+ ? Math.sign(p1.y - prevP.y)
41
+ : Math.sign(p1.x - prevP.x)
42
+
43
+ if (
44
+ (seg1Vertical && prevSegVertical && seg1Direction !== prevDirection) ||
45
+ (!seg1Vertical && !prevSegVertical && seg1Direction !== prevDirection)
46
+ ) {
47
+ isStairStep = false
48
+ break
49
+ }
50
+ }
51
+
52
+ endIdx = i + 2
53
+ }
54
+
55
+ return isStairStep && endIdx - startIdx >= 3 ? endIdx : -1
56
+ }