@tscircuit/schematic-trace-solver 0.0.97 → 0.0.99

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 (50) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.js +1077 -519
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +30 -0
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
  6. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +45 -1
  7. package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +55 -13
  8. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +66 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +124 -0
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +108 -0
  11. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +121 -0
  12. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +114 -0
  13. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +27 -0
  14. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts +30 -0
  15. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/scoreRailAlignment.ts +67 -0
  16. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/types.ts +34 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +1 -60
  18. package/lib/utils/doesPathCoincideWithTraces.ts +61 -0
  19. package/package.json +1 -1
  20. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
  21. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
  22. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
  23. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
  24. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -1
  25. package/tests/examples/__snapshots__/example02.snap.svg +1 -1
  26. package/tests/examples/__snapshots__/example09.snap.svg +9 -13
  27. package/tests/examples/__snapshots__/example14.snap.svg +1 -1
  28. package/tests/examples/__snapshots__/example18.snap.svg +1 -1
  29. package/tests/examples/__snapshots__/example21.snap.svg +30 -32
  30. package/tests/examples/__snapshots__/example29.snap.svg +13 -19
  31. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  32. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -1
  33. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +1 -1
  34. package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
  35. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +1 -1
  36. package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
  37. package/tests/repros/repro-rp2040-gamepad-trace-alignment.test.ts +24 -0
  38. package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
  39. package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
  40. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +38 -0
  41. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-component-scope.test.ts +38 -0
  42. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-eligible-traces.test.ts +28 -0
  43. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-horizontal.test.ts +62 -0
  44. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-anchor.test.ts +36 -0
  45. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-label-junction.test.ts +42 -0
  46. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-obstacle.test.ts +28 -0
  47. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +52 -0
  48. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-vertical.test.ts +25 -0
  49. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-visible-length.test.ts +40 -0
  50. package/tests/solvers/TraceCleanupSolver/fixtures/alignSameNetRails.ts +89 -0
@@ -1,3 +1,6 @@
1
+ import { distance } from "@tscircuit/math-utils"
2
+ import type { ConnectivityMap } from "connectivity-map"
3
+ import type { GraphicsObject } from "graphics-debug"
1
4
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
2
5
  import type {
3
6
  InputChip,
@@ -5,17 +8,17 @@ import type {
5
8
  InputProblem,
6
9
  PinId,
7
10
  } from "lib/types/InputProblem"
8
- import { ConnectivityMap } from "connectivity-map"
9
- import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
10
- import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
11
- import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
12
- import type { GraphicsObject } from "graphics-debug"
13
11
  import { getColorFromString } from "lib/utils/getColorFromString"
14
- import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
15
12
  import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDifferentSchematicSections"
13
+ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
14
+ import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
15
+ import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
16
+ import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
16
17
 
17
18
  export type MspConnectionPairId = string
18
19
 
20
+ const getPinPairKey = (pinIds: [PinId, PinId]) => [...pinIds].sort().join("::")
21
+
19
22
  export type MspConnectionPair = {
20
23
  mspPairId: MspConnectionPairId
21
24
  dcConnNetId: string
@@ -36,6 +39,7 @@ export class MspConnectionPairSolver extends BaseSolver {
36
39
 
37
40
  pinMap: Record<string, InputPin & { chipId: string }>
38
41
  userNetIdByPinId: Record<string, string | undefined>
42
+ directConnectionPinPairKeys: Set<string>
39
43
 
40
44
  constructor({ inputProblem }: { inputProblem: InputProblem }) {
41
45
  super()
@@ -62,7 +66,9 @@ export class MspConnectionPairSolver extends BaseSolver {
62
66
 
63
67
  // Build a mapping from PinId to user-provided netId (if any)
64
68
  this.userNetIdByPinId = {}
69
+ this.directConnectionPinPairKeys = new Set()
65
70
  for (const dc of inputProblem.directConnections) {
71
+ this.directConnectionPinPairKeys.add(getPinPairKey(dc.pinIds))
66
72
  if (dc.netId) {
67
73
  const [a, b] = dc.pinIds
68
74
  this.userNetIdByPinId[a] = dc.netId
@@ -105,9 +111,13 @@ export class MspConnectionPairSolver extends BaseSolver {
105
111
  const [pin1, pin2] = directlyConnectedPins
106
112
  const p1 = this.pinMap[pin1!]!
107
113
  const p2 = this.pinMap[pin2!]!
108
- // Enforce max pair distance (use Manhattan to match orthogonal routing metric)
109
- const manhattanDist = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
110
- if (manhattanDist > this.maxMspPairDistance) {
114
+ const pinPairKey = getPinPairKey([pin1!, pin2!])
115
+ // Explicit source traces are classified by straight-line distance when
116
+ // their input is created; named nets retain the orthogonal route metric.
117
+ const pairDistance = this.directConnectionPinPairKeys.has(pinPairKey)
118
+ ? distance(p1, p2)
119
+ : Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
120
+ if (pairDistance > this.maxMspPairDistance) {
111
121
  // Too far apart; skip creating an MSP pair for this net
112
122
  return
113
123
  }
@@ -16,6 +16,7 @@ import {
16
16
  isVertical,
17
17
  segmentOverlapsRectBoundary,
18
18
  } from "./collisions"
19
+ import { generateEndpointCollisionDetours } from "./generateEndpointCollisionDetours"
19
20
  import {
20
21
  type Axis,
21
22
  aabbFromPoints,
@@ -357,6 +358,35 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
357
358
  const isFirstSegment = segIndex === 0
358
359
  const isLastSegment = segIndex === path.length - 2
359
360
 
361
+ if (path.length === 3 && (isFirstSegment || isLastSegment)) {
362
+ const detours = generateEndpointCollisionDetours({
363
+ path,
364
+ collidingSegmentIndex: segIndex,
365
+ obstacle: rect,
366
+ })
367
+ .filter((detour) => {
368
+ const key = pathKey(detour)
369
+ if (this.visited.has(key)) return false
370
+ this.visited.add(key)
371
+ return true
372
+ })
373
+ .sort(
374
+ (a, b) =>
375
+ this.pathLength(a) - this.pathLength(b) ||
376
+ this.getPinBandPenalty(a) - this.getPinBandPenalty(b),
377
+ )
378
+
379
+ for (const detour of detours) {
380
+ const nextCollisionRects = new Set(collisionRects)
381
+ nextCollisionRects.add(rect)
382
+ this.queue.push({
383
+ path: detour,
384
+ collisionRects: nextCollisionRects,
385
+ })
386
+ }
387
+ return
388
+ }
389
+
360
390
  if (isFirstSegment) {
361
391
  // If first segment collides, move the second segment instead
362
392
  if (path.length < 3) {
@@ -0,0 +1,84 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { isHorizontal, isVertical } from "./collisions"
3
+ import { type Axis, midBetweenPointAndRect } from "./mid"
4
+ import type { ObstacleRect } from "./rect"
5
+
6
+ const getSegmentAxis = (start: Point, end: Point): Axis | null => {
7
+ if (isVertical(start, end)) return "x"
8
+ if (isHorizontal(start, end)) return "y"
9
+ return null
10
+ }
11
+
12
+ const hasOnlyNonzeroOrthogonalSegments = (path: Point[]) =>
13
+ path.every((point, index) => {
14
+ const nextPoint = path[index + 1]
15
+ if (!nextPoint) return true
16
+ if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
17
+ return false
18
+ }
19
+ return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0
20
+ })
21
+
22
+ export const generateEndpointCollisionDetours = ({
23
+ path,
24
+ collidingSegmentIndex,
25
+ obstacle,
26
+ }: {
27
+ path: Point[]
28
+ collidingSegmentIndex: number
29
+ obstacle: ObstacleRect
30
+ }): Point[][] => {
31
+ if (path.length !== 3) return []
32
+
33
+ const lastSegmentIndex = path.length - 2
34
+ if (
35
+ collidingSegmentIndex !== 0 &&
36
+ collidingSegmentIndex !== lastSegmentIndex
37
+ ) {
38
+ return []
39
+ }
40
+
41
+ const shouldReverse = collidingSegmentIndex === lastSegmentIndex
42
+ const orderedPath = shouldReverse ? [...path].reverse() : path
43
+ const [start, corner, end] = orderedPath
44
+ const firstSegmentAxis = getSegmentAxis(start!, corner!)
45
+ const secondSegmentAxis = getSegmentAxis(corner!, end!)
46
+ if (!firstSegmentAxis || !secondSegmentAxis) return []
47
+ if (firstSegmentAxis === secondSegmentAxis) return []
48
+
49
+ const escapeCoordinates = [
50
+ ...midBetweenPointAndRect(secondSegmentAxis, start!, obstacle),
51
+ ...midBetweenPointAndRect(secondSegmentAxis, end!, obstacle),
52
+ ]
53
+ const detourCoordinates = [
54
+ ...midBetweenPointAndRect(firstSegmentAxis, start!, obstacle),
55
+ ...midBetweenPointAndRect(firstSegmentAxis, end!, obstacle),
56
+ ]
57
+
58
+ const detours: Point[][] = []
59
+ for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
60
+ for (const detourCoordinate of [...new Set(detourCoordinates)]) {
61
+ const orderedDetour =
62
+ firstSegmentAxis === "y"
63
+ ? [
64
+ start!,
65
+ { x: escapeCoordinate, y: start!.y },
66
+ { x: escapeCoordinate, y: detourCoordinate },
67
+ { x: end!.x, y: detourCoordinate },
68
+ end!,
69
+ ]
70
+ : [
71
+ start!,
72
+ { x: start!.x, y: escapeCoordinate },
73
+ { x: detourCoordinate, y: escapeCoordinate },
74
+ { x: detourCoordinate, y: end!.y },
75
+ end!,
76
+ ]
77
+
78
+ const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour
79
+ if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour)
80
+ }
81
+ }
82
+
83
+ return detours
84
+ }
@@ -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
+ }