@tscircuit/schematic-trace-solver 0.0.202 → 0.0.204

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.js +54 -7
  2. package/lib/solvers/TraceCleanupSolver/getInteriorShortcutPaths.ts +41 -0
  3. package/lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts +7 -0
  4. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +17 -8
  5. package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +25 -3
  6. package/package.json +1 -1
  7. package/site/SchematicTracePipelineSolver/repro-smart-lock-ble-module.page.tsx +7 -0
  8. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +78 -60
  9. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +62 -62
  10. package/tests/bug-reports/bug-report-20260708T055430Z/__snapshots__/bug-report-20260708T055430Z.snap.svg +2 -2
  11. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  12. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
  13. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +10 -10
  14. package/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg +10 -10
  15. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +9 -9
  16. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +8 -8
  17. package/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg +19 -19
  18. package/tests/bug-reports/bug-report-20260916T054012Z/__snapshots__/bug-report-20260916T054012Z.snap.svg +292 -292
  19. package/tests/examples/__snapshots__/example30.snap.svg +123 -123
  20. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +18 -18
  21. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
  22. package/tests/repros/__snapshots__/repro-am3352-06-boot.snap.svg +448 -0
  23. package/tests/repros/__snapshots__/repro-am3352-sheet3-power-rails.snap.svg +2 -2
  24. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +2 -2
  25. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +2 -2
  26. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +2 -2
  27. package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.snap.svg +7 -7
  28. package/tests/repros/__snapshots__/repro-smart-lock-ble-module.snap.svg +465 -0
  29. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +10 -10
  30. package/tests/repros/assets/repro-am3352-06-boot.input.json +1875 -0
  31. package/tests/repros/assets/repro-smart-lock-ble-module.input.json +2183 -0
  32. package/tests/repros/repro-am3352-06-boot.test.ts +43 -0
  33. package/tests/repros/repro-smart-lock-ble-module.test.ts +69 -0
  34. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-fixed-backbone.test.ts +135 -0
  35. package/tests/solvers/TraceCleanupSolver/reroute-generated-net-label-connector-crossings.test.ts +49 -0
  36. package/tests/solvers/TraceElbowTransitionSimplificationSolver/interior-staircase.test.ts +146 -0
package/dist/index.js CHANGED
@@ -6328,10 +6328,6 @@ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
6328
6328
  const groupIsWithinSingleComponent = [...groupTraceIds].every(
6329
6329
  (traceId) => traceMap.get(traceId)?.pins.every((pin) => pin.chipId === group[0].componentId) === true
6330
6330
  );
6331
- const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
6332
- (traceId) => traceMap.get(traceId)?.tracePath.length === 4
6333
- );
6334
- if (!groupHasOnlySingleRailTraces) return null;
6335
6331
  const orientation = group[0].orientation;
6336
6332
  const linkedLabels = getLabelsLinkedToRailGroup(
6337
6333
  group,
@@ -6362,6 +6358,11 @@ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
6362
6358
  const coordinate = group.find(
6363
6359
  (segment) => Math.abs(segment.coordinate - labelCoordinate) <= RAIL_ALIGNMENT_EPSILON
6364
6360
  )?.coordinate ?? labelCoordinate;
6361
+ if (group.some(
6362
+ (segment) => traceMap.get(segment.traceId)?.tracePath.length !== 4 && !nearlyEqual(segment.coordinate, coordinate)
6363
+ )) {
6364
+ return null;
6365
+ }
6365
6366
  const labelIsAnchoredToRoutedBackbone = anchoringLabels.some(
6366
6367
  (label) => label.mspConnectionPairIds.some(
6367
6368
  (traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4
@@ -7810,6 +7811,36 @@ var is4PointRectangle = (path) => {
7810
7811
  return isHVHC || isVHVC;
7811
7812
  };
7812
7813
 
7814
+ // lib/solvers/TraceCleanupSolver/getInteriorShortcutPaths.ts
7815
+ var getInteriorShortcutPaths = (path) => {
7816
+ const candidates = [path];
7817
+ for (let startIndex = 1; startIndex < path.length - 3; startIndex++) {
7818
+ for (let endIndex = startIndex + 2; endIndex < path.length - 1; endIndex++) {
7819
+ for (const connection of tryConnectPoints(
7820
+ path[startIndex],
7821
+ path[endIndex]
7822
+ )) {
7823
+ const candidate = simplifyPath([
7824
+ ...path.slice(0, startIndex),
7825
+ ...connection,
7826
+ ...path.slice(endIndex + 1)
7827
+ ]);
7828
+ const preservesTerminalDirections = [
7829
+ [0, 1],
7830
+ [-1, -2]
7831
+ ].every(([endIndex2, neighborIndex]) => {
7832
+ const endpoint = path.at(endIndex2);
7833
+ const neighbor = path.at(neighborIndex);
7834
+ const candidateNeighbor = candidate.at(neighborIndex);
7835
+ return (neighbor.x - endpoint.x) * (candidateNeighbor.x - endpoint.x) + (neighbor.y - endpoint.y) * (candidateNeighbor.y - endpoint.y) > 0;
7836
+ });
7837
+ if (preservesTerminalDirections) candidates.push(candidate);
7838
+ }
7839
+ }
7840
+ }
7841
+ return candidates;
7842
+ };
7843
+
7813
7844
  // lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts
7814
7845
  var EPS13 = 1e-6;
7815
7846
  var getConnectorObstacleBounds = (connector, segmentIndex, labels) => {
@@ -7902,7 +7933,12 @@ var rerouteGeneratedNetLabelConnectorCrossings = ({
7902
7933
  obstacleBounds,
7903
7934
  chipBounds: [],
7904
7935
  clearance
7905
- }).filter(
7936
+ }).flatMap(
7937
+ (candidate) => getInteriorShortcutPaths(candidate.path).map((path) => ({
7938
+ ...candidate,
7939
+ path
7940
+ }))
7941
+ ).filter(
7906
7942
  (candidate) => getPathLength4(candidate.path) <= getPathLength4(trace.tracePath) + EPS13 && countTurns(candidate.path) <= countTurns(trace.tracePath) + 2 && !isPathCollidingWithObstacles(
7907
7943
  candidate.path,
7908
7944
  componentAndTextObstacles
@@ -15470,7 +15506,17 @@ var TraceElbowTransitionSimplificationSolver = class extends BaseSolver {
15470
15506
  const otherNetTraces = this.outputTraces.filter(
15471
15507
  (otherTrace) => otherTrace.mspPairId !== trace.mspPairId && otherTrace.globalConnNetId !== trace.globalConnNetId
15472
15508
  );
15509
+ const initialPathLength = getPathLength6(tracePath);
15473
15510
  const candidateByPath = /* @__PURE__ */ new Map();
15511
+ for (const candidate of getInteriorShortcutPaths(tracePath)) {
15512
+ if (Math.abs(getPathLength6(candidate) - initialPathLength) > PATH_LENGTH_EPSILON3) {
15513
+ continue;
15514
+ }
15515
+ candidateByPath.set(
15516
+ candidate.map((point) => `${point.x},${point.y}`).join(";"),
15517
+ candidate
15518
+ );
15519
+ }
15474
15520
  const completedReroutes = this.input.completedReroutes.filter(
15475
15521
  (completedReroute) => completedReroute.initialTrace.mspPairId === trace.mspPairId
15476
15522
  );
@@ -15522,7 +15568,6 @@ var TraceElbowTransitionSimplificationSolver = class extends BaseSolver {
15522
15568
  ({ label }) => `${label.globalConnNetId}:${label.netId}`
15523
15569
  )
15524
15570
  );
15525
- const initialPathLength = getPathLength6(tracePath);
15526
15571
  const validCandidates = [...candidateByPath.values()].filter(
15527
15572
  (candidatePath) => {
15528
15573
  const candidateTrace = { ...trace, tracePath: candidatePath };
@@ -15539,7 +15584,9 @@ var TraceElbowTransitionSimplificationSolver = class extends BaseSolver {
15539
15584
  this.input.netLabelPlacements,
15540
15585
  [trace],
15541
15586
  [candidateTrace]
15542
- ) && !isPathCollidingWithObstacles(candidatePath, this.obstacles) && !doesPathCoincideWithTraces(candidatePath, otherNetTraces);
15587
+ ) && !isPathCollidingWithObstacles(candidatePath, this.obstacles) && !doesPathCoincideWithTraces(candidatePath, otherNetTraces) && otherNetTraces.every(
15588
+ (otherTrace) => countPathIntersections(candidatePath, otherTrace.tracePath) <= countPathIntersections(tracePath, otherTrace.tracePath)
15589
+ );
15543
15590
  }
15544
15591
  );
15545
15592
  const getOverlapCount = (candidatePath) => detectTraceLabelOverlap({
@@ -0,0 +1,41 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { simplifyPath } from "./simplifyPath"
3
+ import { tryConnectPoints } from "./tryConnectPoints"
4
+
5
+ export const getInteriorShortcutPaths = (path: Point[]): Point[][] => {
6
+ const candidates: Point[][] = [path]
7
+ // Reconnect interior points to remove old loops left behind by a connector detour.
8
+ for (let startIndex = 1; startIndex < path.length - 3; startIndex++) {
9
+ for (
10
+ let endIndex = startIndex + 2;
11
+ endIndex < path.length - 1;
12
+ endIndex++
13
+ ) {
14
+ for (const connection of tryConnectPoints(
15
+ path[startIndex]!,
16
+ path[endIndex]!,
17
+ )) {
18
+ const candidate = simplifyPath([
19
+ ...path.slice(0, startIndex),
20
+ ...connection,
21
+ ...path.slice(endIndex + 1),
22
+ ])
23
+ const preservesTerminalDirections = [
24
+ [0, 1],
25
+ [-1, -2],
26
+ ].every(([endIndex, neighborIndex]) => {
27
+ const endpoint = path.at(endIndex!)!
28
+ const neighbor = path.at(neighborIndex!)!
29
+ const candidateNeighbor = candidate.at(neighborIndex!)!
30
+ return (
31
+ (neighbor.x - endpoint.x) * (candidateNeighbor.x - endpoint.x) +
32
+ (neighbor.y - endpoint.y) * (candidateNeighbor.y - endpoint.y) >
33
+ 0
34
+ )
35
+ })
36
+ if (preservesTerminalDirections) candidates.push(candidate)
37
+ }
38
+ }
39
+ }
40
+ return candidates
41
+ }
@@ -9,6 +9,7 @@ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/Schemati
9
9
  import type { InputProblem } from "lib/types/InputProblem"
10
10
  import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
11
11
  import { countTurns } from "./countTurns"
12
+ import { getInteriorShortcutPaths } from "./getInteriorShortcutPaths"
12
13
  import { hasCollisionsWithLabels } from "./hasCollisionsWithLabels"
13
14
  import { findPerpendicularPathCrossings } from "./sub-solver/findIntersectionsWithObstacles"
14
15
  import { generatePerpendicularTraceDetours } from "./sub-solver/generateLShapeRerouteCandidates"
@@ -150,6 +151,12 @@ export const rerouteGeneratedNetLabelConnectorCrossings = ({
150
151
  chipBounds: [],
151
152
  clearance,
152
153
  })
154
+ .flatMap((candidate) =>
155
+ getInteriorShortcutPaths(candidate.path).map((path) => ({
156
+ ...candidate,
157
+ path,
158
+ })),
159
+ )
153
160
  .filter(
154
161
  (candidate) =>
155
162
  getPathLength(candidate.path) <=
@@ -2,7 +2,11 @@ import { distance } from "@tscircuit/math-utils"
2
2
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
3
  import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
4
4
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
- import { getDistinctCoordinates, RAIL_ALIGNMENT_EPSILON } from "./geometry"
5
+ import {
6
+ getDistinctCoordinates,
7
+ nearlyEqual,
8
+ RAIL_ALIGNMENT_EPSILON,
9
+ } from "./geometry"
6
10
  import type { RailSegment } from "./types"
7
11
 
8
12
  const getTransitivelyConnectedTraceIds = (
@@ -59,13 +63,6 @@ export const getFixedLabelCoordinate = (
59
63
  .get(traceId)
60
64
  ?.pins.every((pin) => pin.chipId === group[0]!.componentId) === true,
61
65
  )
62
- const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
63
- (traceId) => traceMap.get(traceId)?.tracePath.length === 4,
64
- )
65
- // A fixed label may lengthen the endpoint legs around one movable rail. A
66
- // trace with more elbows still has to pass the normal readability scoring.
67
- if (!groupHasOnlySingleRailTraces) return null
68
-
69
66
  const orientation = group[0]!.orientation
70
67
  const linkedLabels = getLabelsLinkedToRailGroup(
71
68
  group,
@@ -107,6 +104,18 @@ export const getFixedLabelCoordinate = (
107
104
  Math.abs(segment.coordinate - labelCoordinate) <=
108
105
  RAIL_ALIGNMENT_EPSILON,
109
106
  )?.coordinate ?? labelCoordinate
107
+ // A fixed label may lengthen the endpoint legs around one movable rail.
108
+ // A multi-turn backbone may share the group when it already lies on that
109
+ // coordinate, but moving its elbows still requires normal readability scoring.
110
+ if (
111
+ group.some(
112
+ (segment) =>
113
+ traceMap.get(segment.traceId)?.tracePath.length !== 4 &&
114
+ !nearlyEqual(segment.coordinate, coordinate),
115
+ )
116
+ ) {
117
+ return null
118
+ }
110
119
  const labelIsAnchoredToRoutedBackbone = anchoringLabels.some((label) =>
111
120
  label.mspConnectionPairIds.some(
112
121
  (traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4,
@@ -1,11 +1,13 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import type { GraphicsObject } from "graphics-debug"
3
3
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
+ import { countPathIntersections } from "lib/solvers/Example28Solver/geometry"
4
5
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
5
6
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
7
  import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
7
8
  import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
8
9
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
10
+ import { getInteriorShortcutPaths } from "lib/solvers/TraceCleanupSolver/getInteriorShortcutPaths"
9
11
  import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
10
12
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
11
13
  import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
@@ -82,7 +84,24 @@ export class TraceElbowTransitionSimplificationSolver extends BaseSolver {
82
84
  otherTrace.mspPairId !== trace.mspPairId &&
83
85
  otherTrace.globalConnNetId !== trace.globalConnNetId,
84
86
  )
87
+ const initialPathLength = getPathLength(tracePath)
85
88
  const candidateByPath = new Map<string, Point[]>()
89
+ // Consecutive label detours can leave a staircase between two clear legs.
90
+ // Keep equivalent-length shortcuts, as with the transition candidates below;
91
+ // shortening the route belongs to turn minimization. Terminal directions,
92
+ // label anchors, and collision checks still apply.
93
+ for (const candidate of getInteriorShortcutPaths(tracePath)) {
94
+ if (
95
+ Math.abs(getPathLength(candidate) - initialPathLength) >
96
+ PATH_LENGTH_EPSILON
97
+ ) {
98
+ continue
99
+ }
100
+ candidateByPath.set(
101
+ candidate.map((point) => `${point.x},${point.y}`).join(";"),
102
+ candidate,
103
+ )
104
+ }
86
105
  const completedReroutes = this.input.completedReroutes.filter(
87
106
  (completedReroute) =>
88
107
  completedReroute.initialTrace.mspPairId === trace.mspPairId,
@@ -145,8 +164,6 @@ export class TraceElbowTransitionSimplificationSolver extends BaseSolver {
145
164
  ({ label }) => `${label.globalConnNetId}:${label.netId}`,
146
165
  ),
147
166
  )
148
- const initialPathLength = getPathLength(tracePath)
149
-
150
167
  const validCandidates = [...candidateByPath.values()].filter(
151
168
  (candidatePath) => {
152
169
  const candidateTrace = { ...trace, tracePath: candidatePath }
@@ -175,7 +192,12 @@ export class TraceElbowTransitionSimplificationSolver extends BaseSolver {
175
192
  [candidateTrace],
176
193
  ) &&
177
194
  !isPathCollidingWithObstacles(candidatePath, this.obstacles) &&
178
- !doesPathCoincideWithTraces(candidatePath, otherNetTraces)
195
+ !doesPathCoincideWithTraces(candidatePath, otherNetTraces) &&
196
+ otherNetTraces.every(
197
+ (otherTrace) =>
198
+ countPathIntersections(candidatePath, otherTrace.tracePath) <=
199
+ countPathIntersections(tracePath, otherTrace.tracePath),
200
+ )
179
201
  )
180
202
  },
181
203
  )
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.202",
8
+ "version": "0.0.204",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,7 @@
1
+ import type { InputProblem } from "lib/types/InputProblem"
2
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
3
+ import input from "../../tests/repros/assets/repro-smart-lock-ble-module.input.json"
4
+
5
+ export default () => (
6
+ <PipelineDebugger inputProblem={input as unknown as InputProblem} />
7
+ )