@tscircuit/schematic-trace-solver 0.0.96 → 0.0.98

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 (45) hide show
  1. package/dist/index.d.ts +9 -0
  2. package/dist/index.js +986 -479
  3. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +55 -14
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +31 -0
  5. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +45 -1
  6. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +55 -13
  7. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +66 -0
  8. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +124 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +108 -0
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +121 -0
  11. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +114 -0
  12. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +27 -0
  13. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts +30 -0
  14. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/scoreRailAlignment.ts +67 -0
  15. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/types.ts +34 -0
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -60
  17. package/lib/utils/doesPathCoincideWithTraces.ts +61 -0
  18. package/package.json +1 -1
  19. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +1 -1
  20. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -1
  21. package/tests/examples/__snapshots__/example02.snap.svg +1 -1
  22. package/tests/examples/__snapshots__/example14.snap.svg +1 -1
  23. package/tests/examples/__snapshots__/example18.snap.svg +1 -1
  24. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  25. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -1
  26. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +178 -0
  27. package/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg +54 -0
  28. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
  29. package/tests/repros/assets/repro-rp2040-gamepad-trace-alignment.input.json +237 -0
  30. package/tests/repros/assets/repro-rp2040-zero-crystal-fallback-netlabels.input.json +121 -0
  31. package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +38 -0
  32. package/tests/repros/repro-rp2040-zero-crystal-fallback-netlabels.test.ts +16 -0
  33. package/tests/solvers/SchematicTraceSingleLineSolver2/segment-intersects-rect-interior.test.ts +15 -0
  34. package/tests/solvers/SchematicTraceSingleLineSolver2/segment-overlaps-rect-boundary-boundary.test.ts +18 -0
  35. package/tests/solvers/SchematicTraceSingleLineSolver2/segment-overlaps-rect-boundary-interior.test.ts +18 -0
  36. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-component-scope.test.ts +38 -0
  37. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-eligible-traces.test.ts +28 -0
  38. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-horizontal.test.ts +62 -0
  39. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-anchor.test.ts +36 -0
  40. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-junction.test.ts +42 -0
  41. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-obstacle.test.ts +28 -0
  42. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +52 -0
  43. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-vertical.test.ts +25 -0
  44. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-visible-length.test.ts +40 -0
  45. package/tests/solvers/TraceCleanupSolver/fixtures/alignSameNetRails.ts +89 -0
@@ -1,24 +1,29 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { calculateElbow } from "calculate-elbow"
1
3
  import type { GraphicsObject } from "graphics-debug"
2
- import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
3
4
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
5
  import type { MspConnectionPair } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
5
- import type { InputChip, InputProblem, PinId } from "lib/types/InputProblem"
6
- import type { Point } from "@tscircuit/math-utils"
7
- import { calculateElbow } from "calculate-elbow"
6
+ import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
7
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
+ import type { InputChip, InputProblem } from "lib/types/InputProblem"
9
+ import type { FacingDirection } from "lib/utils/dir"
10
+ import type { RectPadding } from "lib/utils/textBoxBounds"
8
11
  import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
9
- import { getObstacleRects, type ObstacleRect } from "./rect"
10
- import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
12
+ import { calculateDirectShortPath } from "./calculateDirectShortPath"
11
13
  import {
14
+ findFirstCollision,
15
+ isHorizontal,
16
+ isVertical,
17
+ segmentOverlapsRectBoundary,
18
+ } from "./collisions"
19
+ import {
20
+ type Axis,
12
21
  aabbFromPoints,
13
22
  candidateMidsFromSet,
14
23
  midBetweenPointAndRect,
15
- type Axis,
16
24
  } from "./mid"
17
25
  import { pathKey, shiftSegmentOrth } from "./pathOps"
18
- import type { FacingDirection } from "lib/utils/dir"
19
- import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
20
- import type { RectPadding } from "lib/utils/textBoxBounds"
21
- import { calculateDirectShortPath } from "./calculateDirectShortPath"
26
+ import { getObstacleRects, type ObstacleRect } from "./rect"
22
27
 
23
28
  type PathKey = string
24
29
 
@@ -268,9 +273,42 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
268
273
  const collision = findFirstCollision(path, this.obstacles, {
269
274
  excludeRectsForSegment: (segIndex) => {
270
275
  const lastSegIndex = path.length - 2
276
+ const excludedRects = new Set<ObstacleRect>()
277
+
271
278
  if (segIndex === 0 || segIndex === lastSegIndex) {
272
- return this.textObstacles
279
+ for (const textObstacle of this.textObstacles) {
280
+ excludedRects.add(textObstacle)
281
+ }
273
282
  }
283
+
284
+ const segmentStart = path[segIndex]!
285
+ const segmentEnd = path[segIndex + 1]!
286
+ const endpointEpsilon = 1e-9
287
+ const segmentTouchesPin = (pin: MspConnectionPair["pins"][number]) =>
288
+ [segmentStart, segmentEnd].some(
289
+ (point) =>
290
+ Math.abs(point.x - pin.x) <= endpointEpsilon &&
291
+ Math.abs(point.y - pin.y) <= endpointEpsilon,
292
+ )
293
+
294
+ for (const pin of this.pins) {
295
+ if (!segmentTouchesPin(pin)) continue
296
+ const endpointChipObstacle = this.obstacles.find(
297
+ (obstacle) =>
298
+ obstacle.kind === "chip" && obstacle.chipId === pin.chipId,
299
+ )
300
+ if (
301
+ endpointChipObstacle &&
302
+ segmentOverlapsRectBoundary(
303
+ segmentStart,
304
+ segmentEnd,
305
+ endpointChipObstacle,
306
+ )
307
+ ) {
308
+ excludedRects.add(endpointChipObstacle)
309
+ }
310
+ }
311
+
274
312
  const adjacentEndpointSegIndex =
275
313
  segIndex === 1
276
314
  ? 2
@@ -286,9 +324,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
286
324
  path[adjacentEndpointSegIndex + 1]!,
287
325
  )
288
326
  ) {
289
- return this.endpointTextObstacles
327
+ for (const textObstacle of this.endpointTextObstacles) {
328
+ excludedRects.add(textObstacle)
329
+ }
290
330
  }
291
- return new Set<ObstacleRect>()
331
+
332
+ return excludedRects
292
333
  },
293
334
  })
294
335
 
@@ -35,6 +35,37 @@ export const segmentIntersectsRect = <TRect extends RectBounds>(
35
35
  }
36
36
  }
37
37
 
38
+ export const segmentOverlapsRectBoundary = <TRect extends RectBounds>(
39
+ a: Point,
40
+ b: Point,
41
+ r: TRect,
42
+ eps = EPS,
43
+ ): boolean => {
44
+ if (isVertical(a, b, eps)) {
45
+ const onVerticalBoundary =
46
+ Math.abs(a.x - r.minX) <= eps || Math.abs(a.x - r.maxX) <= eps
47
+ if (!onVerticalBoundary) return false
48
+
49
+ const overlap =
50
+ Math.min(Math.max(a.y, b.y), r.maxY) -
51
+ Math.max(Math.min(a.y, b.y), r.minY)
52
+ return overlap > eps
53
+ }
54
+
55
+ if (isHorizontal(a, b, eps)) {
56
+ const onHorizontalBoundary =
57
+ Math.abs(a.y - r.minY) <= eps || Math.abs(a.y - r.maxY) <= eps
58
+ if (!onHorizontalBoundary) return false
59
+
60
+ const overlap =
61
+ Math.min(Math.max(a.x, b.x), r.maxX) -
62
+ Math.max(Math.min(a.x, b.x), r.minX)
63
+ return overlap > eps
64
+ }
65
+
66
+ return false
67
+ }
68
+
38
69
  export const findFirstCollision = <TRect extends RectBounds>(
39
70
  pts: Point[],
40
71
  rects: TRect[],
@@ -79,7 +79,9 @@ export class SchematicTracePipelineSolver extends BaseSolver {
79
79
  availableNetOrientationSolver?: AvailableNetOrientationSolver
80
80
  railNetLabelCornerPlacementSolver?: RailNetLabelCornerPlacementSolver
81
81
  traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
82
+ preAlignmentNetLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
82
83
  netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
84
+ traceCleanupSolver2?: TraceCleanupSolver
83
85
  netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
84
86
 
85
87
  startTimeOfPhase: Record<string, number>
@@ -291,7 +293,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
291
293
  ],
292
294
  ),
293
295
  definePipelineStep(
294
- "netLabelTraceCollisionSolver",
296
+ "preAlignmentNetLabelTraceCollisionSolver",
295
297
  NetLabelTraceCollisionSolver,
296
298
  (instance) => [
297
299
  {
@@ -303,6 +305,48 @@ export class SchematicTracePipelineSolver extends BaseSolver {
303
305
  },
304
306
  ],
305
307
  ),
308
+ definePipelineStep(
309
+ "traceCleanupSolver2",
310
+ TraceCleanupSolver,
311
+ (instance) => {
312
+ const collisionOutput =
313
+ instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
314
+ const labelMergingOutput =
315
+ instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
316
+
317
+ return [
318
+ {
319
+ inputProblem: instance.inputProblem,
320
+ allTraces: collisionOutput.traces,
321
+ allLabelPlacements: collisionOutput.netLabelPlacements,
322
+ mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
323
+ paddingBuffer: 0.1,
324
+ operations: ["aligning_same_net_rails"],
325
+ eligibleTraceIds: new Set(
326
+ instance
327
+ .traceCleanupSolver!.getOutput()
328
+ .traces.map((trace) => trace.mspPairId),
329
+ ),
330
+ },
331
+ ]
332
+ },
333
+ ),
334
+ definePipelineStep(
335
+ "netLabelTraceCollisionSolver",
336
+ NetLabelTraceCollisionSolver,
337
+ (instance) => {
338
+ const previousCollisionOutput =
339
+ instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
340
+
341
+ return [
342
+ {
343
+ inputProblem: instance.inputProblem,
344
+ traces: instance.traceCleanupSolver2!.getOutput().traces,
345
+ netLabelPlacements: previousCollisionOutput.netLabelPlacements,
346
+ },
347
+ ]
348
+ },
349
+ ),
306
350
  definePipelineStep(
307
351
  "netLabelNetLabelCollisionSolver",
308
352
  NetLabelNetLabelCollisionSolver,
@@ -6,16 +6,25 @@ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
6
6
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
7
7
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
8
  import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
9
+ import { alignSameNetRails } from "./alignSameNetRails"
10
+
11
+ export type TraceCleanupOperation =
12
+ | "untangling_traces"
13
+ | "minimizing_turns"
14
+ | "balancing_l_shapes"
15
+ | "aligning_same_net_rails"
9
16
 
10
17
  /**
11
18
  * Defines the input structure for the TraceCleanupSolver.
12
19
  */
13
- interface TraceCleanupSolverInput {
20
+ export interface TraceCleanupSolverInput {
14
21
  inputProblem: InputProblem
15
22
  allTraces: SolvedTracePath[]
16
23
  allLabelPlacements: NetLabelPlacement[]
17
24
  mergedLabelNetIdMap: Record<string, Set<string>>
18
25
  paddingBuffer: number
26
+ operations?: readonly TraceCleanupOperation[]
27
+ eligibleTraceIds?: ReadonlySet<string>
19
28
  }
20
29
 
21
30
  import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
@@ -24,10 +33,11 @@ import { is4PointRectangle } from "./is4PointRectangle"
24
33
  /**
25
34
  * Represents the different stages or steps within the trace cleanup pipeline.
26
35
  */
27
- type PipelineStep =
28
- | "minimizing_turns"
29
- | "balancing_l_shapes"
30
- | "untangling_traces"
36
+ const DEFAULT_OPERATIONS: readonly TraceCleanupOperation[] = [
37
+ "untangling_traces",
38
+ "minimizing_turns",
39
+ "balancing_l_shapes",
40
+ ]
31
41
 
32
42
  /**
33
43
  * The TraceCleanupSolver is responsible for improving the aesthetics and readability of schematic traces.
@@ -42,7 +52,9 @@ export class TraceCleanupSolver extends BaseSolver {
42
52
  private outputTraces: SolvedTracePath[]
43
53
  private traceIdQueue: string[]
44
54
  private tracesMap: Map<string, SolvedTracePath>
45
- private pipelineStep: PipelineStep = "untangling_traces"
55
+ private operations: readonly TraceCleanupOperation[]
56
+ private operationIndex = 0
57
+ private pipelineStep: TraceCleanupOperation | null
46
58
  private activeTraceId: string | null = null // New property
47
59
  override activeSubSolver: BaseSolver | null = null
48
60
 
@@ -51,6 +63,8 @@ export class TraceCleanupSolver extends BaseSolver {
51
63
  this.input = solverInput
52
64
  this.outputTraces = [...solverInput.allTraces]
53
65
  this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
66
+ this.operations = solverInput.operations ?? DEFAULT_OPERATIONS
67
+ this.pipelineStep = this.operations[0] ?? null
54
68
  this.traceIdQueue = Array.from(
55
69
  solverInput.allTraces.map((e) => e.mspPairId),
56
70
  )
@@ -66,14 +80,19 @@ export class TraceCleanupSolver extends BaseSolver {
66
80
  this.outputTraces = output.traces
67
81
  this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
68
82
  this.activeSubSolver = null
69
- this.pipelineStep = "minimizing_turns"
83
+ this._advancePipeline()
70
84
  } else if (this.activeSubSolver.failed) {
71
85
  this.activeSubSolver = null
72
- this.pipelineStep = "minimizing_turns"
86
+ this._advancePipeline()
73
87
  }
74
88
  return
75
89
  }
76
90
 
91
+ if (!this.pipelineStep) {
92
+ this.solved = true
93
+ return
94
+ }
95
+
77
96
  switch (this.pipelineStep) {
78
97
  case "untangling_traces":
79
98
  this._runUntangleTracesStep()
@@ -84,9 +103,19 @@ export class TraceCleanupSolver extends BaseSolver {
84
103
  case "balancing_l_shapes":
85
104
  this._runBalanceLShapesStep()
86
105
  break
106
+ case "aligning_same_net_rails":
107
+ this._runAlignSameNetRailsStep()
108
+ break
87
109
  }
88
110
  }
89
111
 
112
+ private _advancePipeline() {
113
+ this.operationIndex++
114
+ this.pipelineStep = this.operations[this.operationIndex] ?? null
115
+ this.traceIdQueue = this.outputTraces.map((trace) => trace.mspPairId)
116
+ if (this.pipelineStep) this.activeTraceId = null
117
+ }
118
+
90
119
  private _runUntangleTracesStep() {
91
120
  this.activeSubSolver = new UntangleTraceSubsolver({
92
121
  ...this.input,
@@ -96,10 +125,7 @@ export class TraceCleanupSolver extends BaseSolver {
96
125
 
97
126
  private _runMinimizeTurnsStep() {
98
127
  if (this.traceIdQueue.length === 0) {
99
- this.pipelineStep = "balancing_l_shapes"
100
- this.traceIdQueue = Array.from(
101
- this.input.allTraces.map((e) => e.mspPairId),
102
- )
128
+ this._advancePipeline()
103
129
  return
104
130
  }
105
131
 
@@ -108,7 +134,7 @@ export class TraceCleanupSolver extends BaseSolver {
108
134
 
109
135
  private _runBalanceLShapesStep() {
110
136
  if (this.traceIdQueue.length === 0) {
111
- this.solved = true
137
+ this._advancePipeline()
112
138
  return
113
139
  }
114
140
 
@@ -146,6 +172,22 @@ export class TraceCleanupSolver extends BaseSolver {
146
172
  this.outputTraces = Array.from(this.tracesMap.values())
147
173
  }
148
174
 
175
+ private _runAlignSameNetRailsStep() {
176
+ const alignment = alignSameNetRails({
177
+ inputProblem: this.input.inputProblem,
178
+ traces: this.outputTraces,
179
+ netLabelPlacements: this.input.allLabelPlacements,
180
+ eligibleTraceIds:
181
+ this.input.eligibleTraceIds ??
182
+ new Set(this.outputTraces.map((trace) => trace.mspPairId)),
183
+ })
184
+ this.outputTraces = alignment.traces
185
+ this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
186
+ this.stats.alignedRailGroupCount = alignment.alignedRailGroupCount
187
+ this.stats.alignedTraceCount = alignment.alignedTraceCount
188
+ this._advancePipeline()
189
+ }
190
+
149
191
  getOutput() {
150
192
  return {
151
193
  traces: this.outputTraces,
@@ -0,0 +1,66 @@
1
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
4
+ import type { InputProblem } from "lib/types/InputProblem"
5
+ import { evaluateRailGroup } from "./sameNetRailAlignment/evaluateRailGroup"
6
+ import { getRailGroups } from "./sameNetRailAlignment/getRailGroups"
7
+ import type { AlignmentCandidate } from "./sameNetRailAlignment/types"
8
+
9
+ interface AlignSameNetRailsInput {
10
+ inputProblem: InputProblem
11
+ traces: SolvedTracePath[]
12
+ netLabelPlacements: NetLabelPlacement[]
13
+ eligibleTraceIds: ReadonlySet<string>
14
+ }
15
+
16
+ export const alignSameNetRails = ({
17
+ inputProblem,
18
+ traces,
19
+ netLabelPlacements,
20
+ eligibleTraceIds,
21
+ }: AlignSameNetRailsInput): {
22
+ traces: SolvedTracePath[]
23
+ alignedRailGroupCount: number
24
+ alignedTraceCount: number
25
+ } => {
26
+ let outputTraces = [...traces]
27
+ const obstacles = getObstacleRects(inputProblem)
28
+ const alignedTraceIds = new Set<string>()
29
+ let alignedRailGroupCount = 0
30
+ const maximumPasses = Math.max(
31
+ 1,
32
+ traces.reduce((sum, trace) => sum + trace.tracePath.length, 0),
33
+ )
34
+
35
+ for (let pass = 0; pass < maximumPasses; pass++) {
36
+ const groups = getRailGroups(
37
+ outputTraces,
38
+ eligibleTraceIds,
39
+ inputProblem,
40
+ obstacles,
41
+ )
42
+ let applied: AlignmentCandidate | null = null
43
+
44
+ for (const group of groups) {
45
+ applied = evaluateRailGroup({
46
+ group,
47
+ traces: outputTraces,
48
+ netLabelPlacements,
49
+ obstacles,
50
+ eligibleTraceIds,
51
+ })
52
+ if (applied) break
53
+ }
54
+ if (!applied) break
55
+
56
+ outputTraces = applied.traces
57
+ alignedRailGroupCount++
58
+ for (const traceId of applied.changedTraceIds) alignedTraceIds.add(traceId)
59
+ }
60
+
61
+ return {
62
+ traces: outputTraces,
63
+ alignedRailGroupCount,
64
+ alignedTraceCount: alignedTraceIds.size,
65
+ }
66
+ }
@@ -0,0 +1,124 @@
1
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
4
+ import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
+ import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
6
+ import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
7
+ import { getDistinctCoordinates, pointsEqual } from "./geometry"
8
+ import { moveRailSegments } from "./moveRailSegments"
9
+ import { preservesLabelAnchors } from "./preservesLabelAnchors"
10
+ import {
11
+ getTraceGeometryMetrics,
12
+ isReadabilityImprovement,
13
+ scoreIsBetter,
14
+ } from "./scoreRailAlignment"
15
+ import type { AlignmentCandidate, AlignmentScore, RailSegment } from "./types"
16
+
17
+ interface EvaluateRailGroupInput {
18
+ group: RailSegment[]
19
+ traces: SolvedTracePath[]
20
+ netLabelPlacements: NetLabelPlacement[]
21
+ obstacles: ObstacleRect[]
22
+ eligibleTraceIds: ReadonlySet<string>
23
+ }
24
+
25
+ const tracePathChanged = (
26
+ original: SolvedTracePath,
27
+ candidate: SolvedTracePath,
28
+ ) =>
29
+ original.tracePath.length !== candidate.tracePath.length ||
30
+ candidate.tracePath.some(
31
+ (point, index) => !pointsEqual(point, original.tracePath[index]!),
32
+ )
33
+
34
+ export const evaluateRailGroup = ({
35
+ group,
36
+ traces,
37
+ netLabelPlacements,
38
+ obstacles,
39
+ eligibleTraceIds,
40
+ }: EvaluateRailGroupInput): AlignmentCandidate | null => {
41
+ const groupTraceIds = new Set(group.map((segment) => segment.traceId))
42
+ const originalGroupTraces = traces.filter((trace) =>
43
+ groupTraceIds.has(trace.mspPairId),
44
+ )
45
+ const baseline = getTraceGeometryMetrics(originalGroupTraces, traces)
46
+ const coordinates = getDistinctCoordinates(
47
+ group.map((segment) => segment.coordinate),
48
+ )
49
+ const otherNetTraces = traces.filter(
50
+ (trace) => trace.globalConnNetId !== group[0]!.globalConnNetId,
51
+ )
52
+ const immutableSameNetTraces = traces.filter(
53
+ (trace) =>
54
+ trace.globalConnNetId === group[0]!.globalConnNetId &&
55
+ !eligibleTraceIds.has(trace.mspPairId),
56
+ )
57
+
58
+ let best: AlignmentCandidate | null = null
59
+ for (const coordinate of coordinates) {
60
+ const candidateMap = new Map<string, SolvedTracePath>()
61
+
62
+ for (const trace of originalGroupTraces) {
63
+ const candidateTrace = moveRailSegments(
64
+ trace,
65
+ group.filter((segment) => segment.traceId === trace.mspPairId),
66
+ coordinate,
67
+ )
68
+ candidateMap.set(trace.mspPairId, candidateTrace)
69
+ }
70
+
71
+ const candidateTraces = [...candidateMap.values()]
72
+ const allCandidateTraces = traces.map(
73
+ (trace) => candidateMap.get(trace.mspPairId) ?? trace,
74
+ )
75
+ const candidatesAreClear = candidateTraces.every(
76
+ (candidate) =>
77
+ !isPathCollidingWithObstacles(candidate.tracePath, obstacles) &&
78
+ detectTraceLabelOverlap({
79
+ traces: [candidate],
80
+ netLabels: netLabelPlacements,
81
+ }).length === 0 &&
82
+ !doesPathCoincideWithTraces(candidate.tracePath, otherNetTraces) &&
83
+ !doesPathCoincideWithTraces(
84
+ candidate.tracePath,
85
+ immutableSameNetTraces.filter(
86
+ (trace) => trace.mspPairId !== candidate.mspPairId,
87
+ ),
88
+ ),
89
+ )
90
+ if (!candidatesAreClear) continue
91
+ if (
92
+ !preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)
93
+ ) {
94
+ continue
95
+ }
96
+
97
+ const metrics = getTraceGeometryMetrics(candidateTraces, allCandidateTraces)
98
+ if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
99
+ if (!isReadabilityImprovement(metrics, baseline)) continue
100
+
101
+ const score: AlignmentScore = {
102
+ ...metrics,
103
+ displacement: group.reduce(
104
+ (sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
105
+ 0,
106
+ ),
107
+ coordinate,
108
+ }
109
+ const changedTraceIds = candidateTraces
110
+ .filter((candidate) => {
111
+ const original = traces.find(
112
+ (trace) => trace.mspPairId === candidate.mspPairId,
113
+ )!
114
+ return tracePathChanged(original, candidate)
115
+ })
116
+ .map((trace) => trace.mspPairId)
117
+ if (changedTraceIds.length === 0) continue
118
+
119
+ const candidate = { traces: allCandidateTraces, changedTraceIds, score }
120
+ if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
121
+ }
122
+
123
+ return best
124
+ }
@@ -0,0 +1,108 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+ import type { RailOrientation, RailSegment } from "./types"
4
+
5
+ export const RAIL_ALIGNMENT_EPSILON = 2e-3
6
+
7
+ export const nearlyEqual = (a: number, b: number) =>
8
+ Math.abs(a - b) < RAIL_ALIGNMENT_EPSILON
9
+
10
+ export const isHorizontal = (a: Point, b: Point) => nearlyEqual(a.y, b.y)
11
+
12
+ export const isVertical = (a: Point, b: Point) => nearlyEqual(a.x, b.x)
13
+
14
+ const isPositiveLength = (a: Point, b: Point) =>
15
+ Math.abs(a.x - b.x) >= RAIL_ALIGNMENT_EPSILON ||
16
+ Math.abs(a.y - b.y) >= RAIL_ALIGNMENT_EPSILON
17
+
18
+ export const getRailOrientation = (
19
+ a: Point,
20
+ b: Point,
21
+ ): RailOrientation | null => {
22
+ if (!isPositiveLength(a, b)) return null
23
+ if (isVertical(a, b)) return "vertical"
24
+ if (isHorizontal(a, b)) return "horizontal"
25
+ return null
26
+ }
27
+
28
+ export const rangesTouchOrOverlap = (a: RailSegment, b: RailSegment) =>
29
+ Math.min(a.maxAlong, b.maxAlong) - Math.max(a.minAlong, b.minAlong) >=
30
+ -RAIL_ALIGNMENT_EPSILON
31
+
32
+ export const pointsEqual = (a: Point, b: Point) =>
33
+ nearlyEqual(a.x, b.x) && nearlyEqual(a.y, b.y)
34
+
35
+ export const getDistinctCoordinates = (coordinates: number[]) => {
36
+ const distinct: number[] = []
37
+ for (const coordinate of coordinates) {
38
+ if (!distinct.some((item) => nearlyEqual(item, coordinate))) {
39
+ distinct.push(coordinate)
40
+ }
41
+ }
42
+ return distinct
43
+ }
44
+
45
+ interface Interval {
46
+ coordinate: number
47
+ min: number
48
+ max: number
49
+ }
50
+
51
+ const getMergedIntervalLength = (intervals: Interval[]) => {
52
+ const groups: Interval[][] = []
53
+ for (const interval of intervals) {
54
+ const group = groups.find((items) =>
55
+ nearlyEqual(items[0]!.coordinate, interval.coordinate),
56
+ )
57
+ if (group) group.push(interval)
58
+ else groups.push([interval])
59
+ }
60
+
61
+ return groups.reduce((total, group) => {
62
+ const sorted = [...group].sort((a, b) => a.min - b.min)
63
+ let groupLength = 0
64
+ let currentMin = sorted[0]!.min
65
+ let currentMax = sorted[0]!.max
66
+
67
+ for (const interval of sorted.slice(1)) {
68
+ if (interval.min <= currentMax + RAIL_ALIGNMENT_EPSILON) {
69
+ currentMax = Math.max(currentMax, interval.max)
70
+ } else {
71
+ groupLength += currentMax - currentMin
72
+ currentMin = interval.min
73
+ currentMax = interval.max
74
+ }
75
+ }
76
+
77
+ return total + groupLength + currentMax - currentMin
78
+ }, 0)
79
+ }
80
+
81
+ /** Returns rendered length after overlapping collinear runs are merged. */
82
+ export const getVisibleTraceLength = (traces: SolvedTracePath[]) => {
83
+ const horizontal: Interval[] = []
84
+ const vertical: Interval[] = []
85
+
86
+ for (const trace of traces) {
87
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
88
+ const start = trace.tracePath[index]!
89
+ const end = trace.tracePath[index + 1]!
90
+
91
+ if (isHorizontal(start, end)) {
92
+ horizontal.push({
93
+ coordinate: start.y,
94
+ min: Math.min(start.x, end.x),
95
+ max: Math.max(start.x, end.x),
96
+ })
97
+ } else if (isVertical(start, end)) {
98
+ vertical.push({
99
+ coordinate: start.x,
100
+ min: Math.min(start.y, end.y),
101
+ max: Math.max(start.y, end.y),
102
+ })
103
+ }
104
+ }
105
+ }
106
+
107
+ return getMergedIntervalLength(horizontal) + getMergedIntervalLength(vertical)
108
+ }