@tscircuit/schematic-trace-solver 0.0.97 → 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 (36) hide show
  1. package/dist/index.d.ts +9 -0
  2. package/dist/index.js +753 -285
  3. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +45 -1
  4. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +55 -13
  5. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +66 -0
  6. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +124 -0
  7. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +108 -0
  8. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +121 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +114 -0
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +27 -0
  11. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts +30 -0
  12. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/scoreRailAlignment.ts +67 -0
  13. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/types.ts +34 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -60
  15. package/lib/utils/doesPathCoincideWithTraces.ts +61 -0
  16. package/package.json +1 -1
  17. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +1 -1
  18. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -1
  19. package/tests/examples/__snapshots__/example02.snap.svg +1 -1
  20. package/tests/examples/__snapshots__/example14.snap.svg +1 -1
  21. package/tests/examples/__snapshots__/example18.snap.svg +1 -1
  22. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  23. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -1
  24. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +1 -1
  25. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
  26. package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +24 -0
  27. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-component-scope.test.ts +38 -0
  28. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-eligible-traces.test.ts +28 -0
  29. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-horizontal.test.ts +62 -0
  30. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-anchor.test.ts +36 -0
  31. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-junction.test.ts +42 -0
  32. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-obstacle.test.ts +28 -0
  33. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +52 -0
  34. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-vertical.test.ts +25 -0
  35. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-visible-length.test.ts +40 -0
  36. package/tests/solvers/TraceCleanupSolver/fixtures/alignSameNetRails.ts +89 -0
@@ -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
+ }
@@ -0,0 +1,121 @@
1
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
3
+ import type { InputChip } from "lib/types/InputProblem"
4
+ import type { FacingDirection } from "lib/utils/dir"
5
+ import { getRailOrientation, RAIL_ALIGNMENT_EPSILON } from "./geometry"
6
+ import type { RailSegment } from "./types"
7
+
8
+ type UnassociatedRailSegment = Omit<
9
+ RailSegment,
10
+ "componentId" | "componentFacingDirection"
11
+ >
12
+
13
+ const getMovableRailSegments = (
14
+ trace: SolvedTracePath,
15
+ ): UnassociatedRailSegment[] => {
16
+ const segments: UnassociatedRailSegment[] = []
17
+ const path = trace.tracePath
18
+
19
+ for (let segmentIndex = 1; segmentIndex < path.length - 2; segmentIndex++) {
20
+ const previous = path[segmentIndex - 1]!
21
+ const start = path[segmentIndex]!
22
+ const end = path[segmentIndex + 1]!
23
+ const next = path[segmentIndex + 2]!
24
+ const orientation = getRailOrientation(start, end)
25
+ if (!orientation) continue
26
+
27
+ const previousOrientation = getRailOrientation(previous, start)
28
+ const nextOrientation = getRailOrientation(end, next)
29
+ if (!previousOrientation || !nextOrientation) continue
30
+ if (previousOrientation === orientation || nextOrientation === orientation)
31
+ continue
32
+
33
+ const alongValues =
34
+ orientation === "vertical" ? [start.y, end.y] : [start.x, end.x]
35
+ segments.push({
36
+ traceId: trace.mspPairId,
37
+ segmentIndex,
38
+ globalConnNetId: trace.globalConnNetId,
39
+ orientation,
40
+ coordinate: orientation === "vertical" ? start.x : start.y,
41
+ minAlong: Math.min(...alongValues),
42
+ maxAlong: Math.max(...alongValues),
43
+ })
44
+ }
45
+
46
+ return segments
47
+ }
48
+
49
+ const railIsOutsideComponent = (
50
+ segment: UnassociatedRailSegment,
51
+ chip: InputChip,
52
+ facingDirection: FacingDirection,
53
+ ) => {
54
+ const minX = chip.center.x - chip.width / 2
55
+ const maxX = chip.center.x + chip.width / 2
56
+ const minY = chip.center.y - chip.height / 2
57
+ const maxY = chip.center.y + chip.height / 2
58
+
59
+ switch (facingDirection) {
60
+ case "x-":
61
+ return (
62
+ segment.orientation === "vertical" &&
63
+ segment.coordinate <= minX + RAIL_ALIGNMENT_EPSILON
64
+ )
65
+ case "x+":
66
+ return (
67
+ segment.orientation === "vertical" &&
68
+ segment.coordinate >= maxX - RAIL_ALIGNMENT_EPSILON
69
+ )
70
+ case "y+":
71
+ return (
72
+ segment.orientation === "horizontal" &&
73
+ segment.coordinate >= maxY - RAIL_ALIGNMENT_EPSILON
74
+ )
75
+ case "y-":
76
+ return (
77
+ segment.orientation === "horizontal" &&
78
+ segment.coordinate <= minY + RAIL_ALIGNMENT_EPSILON
79
+ )
80
+ }
81
+ }
82
+
83
+ /** Associates each movable internal rail with the nearest component endpoint. */
84
+ export const getComponentSideRailSegments = (
85
+ trace: SolvedTracePath,
86
+ chipMap: Map<string, InputChip>,
87
+ ): RailSegment[] => {
88
+ const segments: RailSegment[] = []
89
+
90
+ for (const segment of getMovableRailSegments(trace)) {
91
+ const associations = trace.pins.flatMap((pin, pinIndex) => {
92
+ const chip = chipMap.get(pin.chipId)
93
+ if (!chip) return []
94
+
95
+ const componentFacingDirection =
96
+ pin._facingDirection ?? getPinDirection(pin, chip)
97
+ if (!railIsOutsideComponent(segment, chip, componentFacingDirection)) {
98
+ return []
99
+ }
100
+
101
+ return [
102
+ {
103
+ componentId: chip.chipId,
104
+ componentFacingDirection,
105
+ distanceFromEndpoint:
106
+ pinIndex === 0
107
+ ? segment.segmentIndex
108
+ : trace.tracePath.length - 2 - segment.segmentIndex,
109
+ },
110
+ ]
111
+ })
112
+
113
+ associations.sort((a, b) => a.distanceFromEndpoint - b.distanceFromEndpoint)
114
+ const association = associations[0]
115
+ if (!association) continue
116
+
117
+ segments.push({ ...segment, ...association })
118
+ }
119
+
120
+ return segments
121
+ }