@tscircuit/schematic-trace-solver 0.0.30 → 0.0.32

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.
package/CLAUDE.md ADDED
@@ -0,0 +1,8 @@
1
+ - use `bunx tsc --noEmit` to type check
2
+ - use `bun test path/to/test.ts` to run a test
3
+ - use `bun add <package>` to add a package
4
+
5
+ There are two ways the user tests the algorithm:
6
+
7
+ - Running `bun test`
8
+ - Looking at the `*.page.tsx` files in the `site` directory via a browser
package/dist/index.js CHANGED
@@ -607,7 +607,8 @@ var cartesian = (arrays) => arrays.length === 0 ? [[]] : arrays.reduce(
607
607
  );
608
608
  var generateElbowVariants = ({
609
609
  baseElbow,
610
- guidelines
610
+ guidelines,
611
+ maxVariants = 1e4
611
612
  }) => {
612
613
  const elbow = preprocessElbow(baseElbow);
613
614
  assertOrthogonalPolyline(elbow);
@@ -692,6 +693,9 @@ var generateElbowVariants = ({
692
693
  if (!seen.has(key)) {
693
694
  seen.add(key);
694
695
  elbowVariants.push(variant);
696
+ if (elbowVariants.length >= maxVariants) {
697
+ break;
698
+ }
695
699
  }
696
700
  }
697
701
  return { elbowVariants, movableSegments };
@@ -868,7 +872,8 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
868
872
  );
869
873
  const { elbowVariants, movableSegments } = generateElbowVariants({
870
874
  baseElbow: this.baseElbow,
871
- guidelines: this.guidelines
875
+ guidelines: this.guidelines,
876
+ maxVariants: 1e3
872
877
  });
873
878
  this.movableSegments = movableSegments;
874
879
  const getPathLength = (pts) => {
@@ -906,6 +911,7 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
906
911
  pinIdMap: this.pinIdMap,
907
912
  chipMap: this.chipMap
908
913
  });
914
+ let pathIsValid = true;
909
915
  for (let i = 0; i < nextCandidatePath.length - 1; i++) {
910
916
  const start = nextCandidatePath[i];
911
917
  const end = nextCandidatePath[i + 1];
@@ -913,12 +919,19 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
913
919
  const EPS2 = 1e-9;
914
920
  for (const [, rcl] of restrictedCenterLines) {
915
921
  if (rcl.axes.has("x") && typeof rcl.x === "number") {
916
- if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS2) return;
922
+ if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS2) {
923
+ pathIsValid = false;
924
+ break;
925
+ }
917
926
  }
918
927
  if (rcl.axes.has("y") && typeof rcl.y === "number") {
919
- if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS2) return;
928
+ if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS2) {
929
+ pathIsValid = false;
930
+ break;
931
+ }
920
932
  }
921
933
  }
934
+ if (!pathIsValid) break;
922
935
  const isStartPin = this.pins.some(
923
936
  (pin) => Math.abs(pin.x - start.x) < 1e-10 && Math.abs(pin.y - start.y) < 1e-10
924
937
  );
@@ -939,12 +952,16 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
939
952
  const onBottom = Math.abs(start.y - bounds.minY) < 1e-9;
940
953
  const onTop = Math.abs(start.y - bounds.maxY) < 1e-9;
941
954
  const entersInterior = onLeft && dx > EPS3 || onRight && dx < -EPS3 || onBottom && dy > EPS3 || onTop && dy < -EPS3;
942
- if (entersInterior) return;
955
+ if (entersInterior) {
956
+ pathIsValid = false;
957
+ break;
958
+ }
943
959
  if (!excludeChipIds.includes(startPin.chipId)) {
944
960
  excludeChipIds.push(startPin.chipId);
945
961
  }
946
962
  }
947
963
  }
964
+ if (!pathIsValid) break;
948
965
  if (isEndPin) {
949
966
  const endPin = this.pins.find(
950
967
  (pin) => Math.abs(pin.x - end.x) < 1e-10 && Math.abs(pin.y - end.y) < 1e-10
@@ -959,21 +976,30 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
959
976
  const onBottom = Math.abs(end.y - bounds.minY) < 1e-9;
960
977
  const onTop = Math.abs(end.y - bounds.maxY) < 1e-9;
961
978
  const entersInterior = onLeft && dx > EPS3 || onRight && dx < -EPS3 || onBottom && dy > EPS3 || onTop && dy < -EPS3;
962
- if (entersInterior) return;
979
+ if (entersInterior) {
980
+ pathIsValid = false;
981
+ break;
982
+ }
963
983
  if (!excludeChipIds.includes(endPin.chipId)) {
964
984
  excludeChipIds.push(endPin.chipId);
965
985
  }
966
986
  }
967
987
  }
988
+ if (!pathIsValid) break;
968
989
  const obstacleOps = { excludeChipIds };
969
990
  const intersects = this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip(
970
991
  [start, end],
971
992
  obstacleOps
972
993
  );
973
- if (intersects) return;
994
+ if (intersects) {
995
+ pathIsValid = false;
996
+ break;
997
+ }
998
+ }
999
+ if (pathIsValid) {
1000
+ this.solvedTracePath = nextCandidatePath;
1001
+ this.solved = true;
974
1002
  }
975
- this.solvedTracePath = nextCandidatePath;
976
- this.solved = true;
977
1003
  }
978
1004
  visualize() {
979
1005
  const graphics = visualizeInputProblem(this.inputProblem, {
@@ -1095,11 +1121,11 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
1095
1121
  return;
1096
1122
  }
1097
1123
  const connectionPair = this.queuedConnectionPairs.shift();
1098
- this.currentConnectionPair = connectionPair;
1099
1124
  if (!connectionPair) {
1100
1125
  this.solved = true;
1101
1126
  return;
1102
1127
  }
1128
+ this.currentConnectionPair = connectionPair;
1103
1129
  const { pins } = connectionPair;
1104
1130
  this.activeSubSolver = new SchematicTraceSingleLineSolver({
1105
1131
  inputProblem: this.inputProblem,
@@ -1137,6 +1163,64 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
1137
1163
  }
1138
1164
  };
1139
1165
 
1166
+ // lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/applyJogToTrace.ts
1167
+ var applyJogToTerminalSegment = ({
1168
+ pts,
1169
+ segmentIndex: si,
1170
+ offset,
1171
+ JOG_SIZE,
1172
+ EPS: EPS2 = 1e-6
1173
+ }) => {
1174
+ if (si !== 0 && si !== pts.length - 2) return;
1175
+ const start = pts[si];
1176
+ const end = pts[si + 1];
1177
+ const isVertical = Math.abs(start.x - end.x) < EPS2;
1178
+ const isHorizontal = Math.abs(start.y - end.y) < EPS2;
1179
+ if (!isVertical && !isHorizontal) return;
1180
+ const segDir = isVertical ? end.y > start.y ? 1 : -1 : end.x > start.x ? 1 : -1;
1181
+ if (si === 0) {
1182
+ if (isVertical) {
1183
+ const jogY = start.y + segDir * JOG_SIZE;
1184
+ pts.splice(
1185
+ 1,
1186
+ 1,
1187
+ { x: start.x, y: jogY },
1188
+ { x: start.x + offset, y: jogY },
1189
+ { x: end.x + offset, y: end.y }
1190
+ );
1191
+ } else {
1192
+ const jogX = start.x + segDir * JOG_SIZE;
1193
+ pts.splice(
1194
+ 1,
1195
+ 1,
1196
+ { x: jogX, y: start.y },
1197
+ { x: jogX, y: start.y + offset },
1198
+ { x: end.x, y: end.y + offset }
1199
+ );
1200
+ }
1201
+ } else {
1202
+ if (isVertical) {
1203
+ const jogY = end.y - segDir * JOG_SIZE;
1204
+ pts.splice(
1205
+ si,
1206
+ 1,
1207
+ { x: start.x + offset, y: start.y },
1208
+ { x: end.x + offset, y: jogY },
1209
+ { x: end.x, y: jogY }
1210
+ );
1211
+ } else {
1212
+ const jogX = end.x - segDir * JOG_SIZE;
1213
+ pts.splice(
1214
+ si,
1215
+ 1,
1216
+ { x: start.x, y: start.y + offset },
1217
+ { x: jogX, y: end.y + offset },
1218
+ { x: jogX, y: end.y }
1219
+ );
1220
+ }
1221
+ }
1222
+ };
1223
+
1140
1224
  // lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts
1141
1225
  var TraceOverlapIssueSolver = class extends BaseSolver {
1142
1226
  overlappingTraceSegments;
@@ -1180,33 +1264,30 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1180
1264
  const current = this.correctedTraceMap[original.mspPairId] ?? original;
1181
1265
  const pts = current.tracePath.map((p) => ({ ...p }));
1182
1266
  const segIdxs = Array.from(segIdxSet).sort((a, b) => a - b);
1183
- const appliedX = /* @__PURE__ */ new Set();
1184
- const appliedY = /* @__PURE__ */ new Set();
1185
- for (const si of segIdxs) {
1267
+ const segIdxsRev = Array.from(segIdxSet).sort((a, b) => a - b).reverse();
1268
+ const JOG_SIZE = this.SHIFT_DISTANCE;
1269
+ for (const si of segIdxsRev) {
1186
1270
  if (si < 0 || si >= pts.length - 1) continue;
1187
- if (si === 0 || si === pts.length - 2) continue;
1188
- const start = pts[si];
1189
- const end = pts[si + 1];
1190
- const isVertical = Math.abs(start.x - end.x) < EPS2;
1191
- const isHorizontal = Math.abs(start.y - end.y) < EPS2;
1192
- if (!isVertical && !isHorizontal) continue;
1193
- if (isVertical) {
1194
- if (!appliedX.has(si)) {
1271
+ if (si === 0 || si === pts.length - 2) {
1272
+ applyJogToTerminalSegment({
1273
+ pts,
1274
+ segmentIndex: si,
1275
+ offset,
1276
+ JOG_SIZE,
1277
+ EPS: EPS2
1278
+ });
1279
+ } else {
1280
+ const start = pts[si];
1281
+ const end = pts[si + 1];
1282
+ const isVertical = Math.abs(start.x - end.x) < EPS2;
1283
+ const isHorizontal = Math.abs(start.y - end.y) < EPS2;
1284
+ if (!isVertical && !isHorizontal) continue;
1285
+ if (isVertical) {
1195
1286
  start.x += offset;
1196
- appliedX.add(si);
1197
- }
1198
- if (!appliedX.has(si + 1)) {
1199
1287
  end.x += offset;
1200
- appliedX.add(si + 1);
1201
- }
1202
- } else if (isHorizontal) {
1203
- if (!appliedY.has(si)) {
1288
+ } else {
1204
1289
  start.y += offset;
1205
- appliedY.add(si);
1206
- }
1207
- if (!appliedY.has(si + 1)) {
1208
1290
  end.y += offset;
1209
- appliedY.add(si + 1);
1210
1291
  }
1211
1292
  }
1212
1293
  }
@@ -1295,7 +1376,7 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
1295
1376
  return islands;
1296
1377
  }
1297
1378
  findNextOverlapIssue() {
1298
- const EPS2 = 1e-6;
1379
+ const EPS2 = 2e-3;
1299
1380
  const netIds = Object.keys(this.traceNetIslands);
1300
1381
  for (let i = 0; i < netIds.length; i++) {
1301
1382
  for (let j = i + 1; j < netIds.length; j++) {
@@ -101,13 +101,14 @@ export class SchematicTraceLinesSolver extends BaseSolver {
101
101
  }
102
102
 
103
103
  const connectionPair = this.queuedConnectionPairs.shift()
104
- this.currentConnectionPair = connectionPair!
105
104
 
106
105
  if (!connectionPair) {
107
106
  this.solved = true
108
107
  return
109
108
  }
110
109
 
110
+ this.currentConnectionPair = connectionPair
111
+
111
112
  const { pins } = connectionPair
112
113
 
113
114
  this.activeSubSolver = new SchematicTraceSingleLineSolver({
@@ -97,6 +97,7 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
97
97
  const { elbowVariants, movableSegments } = generateElbowVariants({
98
98
  baseElbow: this.baseElbow,
99
99
  guidelines: this.guidelines,
100
+ maxVariants: 1000,
100
101
  })
101
102
 
102
103
  this.movableSegments = movableSegments
@@ -144,6 +145,9 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
144
145
  chipMap: this.chipMap,
145
146
  })
146
147
 
148
+ // Check if this candidate path is valid
149
+ let pathIsValid = true
150
+
147
151
  for (let i = 0; i < nextCandidatePath.length - 1; i++) {
148
152
  const start = nextCandidatePath[i]
149
153
  const end = nextCandidatePath[i + 1]
@@ -156,14 +160,22 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
156
160
  for (const [, rcl] of restrictedCenterLines) {
157
161
  if (rcl.axes.has("x") && typeof rcl.x === "number") {
158
162
  // segment strictly crosses vertical center line
159
- if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) return
163
+ if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) {
164
+ pathIsValid = false
165
+ break
166
+ }
160
167
  }
161
168
  if (rcl.axes.has("y") && typeof rcl.y === "number") {
162
169
  // segment strictly crosses horizontal center line
163
- if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) return
170
+ if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) {
171
+ pathIsValid = false
172
+ break
173
+ }
164
174
  }
165
175
  }
166
176
 
177
+ if (!pathIsValid) break
178
+
167
179
  // Always exclude chips that contain the start or end points of this segment
168
180
  // if those points are actually pin locations
169
181
  const isStartPin = this.pins.some(
@@ -197,13 +209,18 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
197
209
  (onRight && dx < -EPS) ||
198
210
  (onBottom && dy > EPS) ||
199
211
  (onTop && dy < -EPS)
200
- if (entersInterior) return
212
+ if (entersInterior) {
213
+ pathIsValid = false
214
+ break
215
+ }
201
216
  if (!excludeChipIds.includes(startPin.chipId)) {
202
217
  excludeChipIds.push(startPin.chipId)
203
218
  }
204
219
  }
205
220
  }
206
221
 
222
+ if (!pathIsValid) break
223
+
207
224
  if (isEndPin) {
208
225
  const endPin = this.pins.find(
209
226
  (pin) =>
@@ -224,24 +241,37 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
224
241
  (onRight && dx < -EPS) ||
225
242
  (onBottom && dy > EPS) ||
226
243
  (onTop && dy < -EPS)
227
- if (entersInterior) return
244
+ if (entersInterior) {
245
+ pathIsValid = false
246
+ break
247
+ }
228
248
  if (!excludeChipIds.includes(endPin.chipId)) {
229
249
  excludeChipIds.push(endPin.chipId)
230
250
  }
231
251
  }
232
252
  }
233
253
 
254
+ if (!pathIsValid) break
255
+
234
256
  const obstacleOps = { excludeChipIds }
235
257
  const intersects =
236
258
  this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip(
237
259
  [start, end],
238
260
  obstacleOps,
239
261
  )
240
- if (intersects) return
262
+ if (intersects) {
263
+ pathIsValid = false
264
+ break
265
+ }
241
266
  }
242
267
 
243
- this.solvedTracePath = nextCandidatePath
244
- this.solved = true
268
+ // If this path is valid, use it as the solution
269
+ if (pathIsValid) {
270
+ this.solvedTracePath = nextCandidatePath
271
+ this.solved = true
272
+ }
273
+ // If this path is invalid, continue to next step to try the next candidate
274
+ // The next _step() call will try the next candidate path
245
275
  }
246
276
 
247
277
  override visualize(): GraphicsObject {
@@ -260,9 +260,11 @@ const cartesian = <T>(arrays: T[][]): T[][] =>
260
260
  export const generateElbowVariants = ({
261
261
  baseElbow,
262
262
  guidelines,
263
+ maxVariants = 10000,
263
264
  }: {
264
265
  baseElbow: Point[]
265
266
  guidelines: Guideline[]
267
+ maxVariants?: number
266
268
  }): {
267
269
  elbowVariants: Array<Point[]>
268
270
  movableSegments: Array<MovableSegment>
@@ -395,6 +397,11 @@ export const generateElbowVariants = ({
395
397
  if (!seen.has(key)) {
396
398
  seen.add(key)
397
399
  elbowVariants.push(variant)
400
+
401
+ // Stop if we've hit the maximum number of variants
402
+ if (elbowVariants.length >= maxVariants) {
403
+ break
404
+ }
398
405
  }
399
406
  }
400
407
 
@@ -2,6 +2,7 @@ import type { GraphicsObject } from "graphics-debug"
2
2
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
3
  import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
4
4
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import { applyJogToTerminalSegment } from "./applyJogToTrace"
5
6
 
6
7
  type ConnNetId = string
7
8
 
@@ -83,38 +84,39 @@ export class TraceOverlapIssueSolver extends BaseSolver {
83
84
 
84
85
  const segIdxs = Array.from(segIdxSet).sort((a, b) => a - b)
85
86
 
86
- // Track per-point adjustments to avoid double-shifting shared joints
87
- const appliedX = new Set<number>()
88
- const appliedY = new Set<number>()
87
+ const segIdxsRev = Array.from(segIdxSet)
88
+ .sort((a, b) => a - b)
89
+ .reverse()
89
90
 
90
- for (const si of segIdxs) {
91
- if (si < 0 || si >= pts.length - 1) continue
92
- // Do not move the first or last segment since they connect directly to pins
93
- if (si === 0 || si === pts.length - 2) continue
94
- const start = pts[si]!
95
- const end = pts[si + 1]!
96
- const isVertical = Math.abs(start.x - end.x) < EPS
97
- const isHorizontal = Math.abs(start.y - end.y) < EPS
91
+ const JOG_SIZE = this.SHIFT_DISTANCE
98
92
 
99
- if (!isVertical && !isHorizontal) continue
93
+ // Process from end to start to keep indices valid after splicing
94
+ for (const si of segIdxsRev) {
95
+ if (si < 0 || si >= pts.length - 1) continue
100
96
 
101
- if (isVertical) {
102
- if (!appliedX.has(si)) {
97
+ if (si === 0 || si === pts.length - 2) {
98
+ applyJogToTerminalSegment({
99
+ pts,
100
+ segmentIndex: si,
101
+ offset,
102
+ JOG_SIZE,
103
+ EPS,
104
+ })
105
+ } else {
106
+ // Internal segment - shift both points
107
+ const start = pts[si]!
108
+ const end = pts[si + 1]!
109
+ const isVertical = Math.abs(start.x - end.x) < EPS
110
+ const isHorizontal = Math.abs(start.y - end.y) < EPS
111
+ if (!isVertical && !isHorizontal) continue
112
+
113
+ if (isVertical) {
103
114
  start.x += offset
104
- appliedX.add(si)
105
- }
106
- if (!appliedX.has(si + 1)) {
107
115
  end.x += offset
108
- appliedX.add(si + 1)
109
- }
110
- } else if (isHorizontal) {
111
- if (!appliedY.has(si)) {
116
+ } else {
117
+ // Horizontal
112
118
  start.y += offset
113
- appliedY.add(si)
114
- }
115
- if (!appliedY.has(si + 1)) {
116
119
  end.y += offset
117
- appliedY.add(si + 1)
118
120
  }
119
121
  }
120
122
  }
@@ -0,0 +1,76 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+
3
+ export const applyJogToTerminalSegment = ({
4
+ pts,
5
+ segmentIndex: si,
6
+ offset,
7
+ JOG_SIZE,
8
+ EPS = 1e-6,
9
+ }: {
10
+ pts: Point[]
11
+ segmentIndex: number
12
+ offset: number
13
+ JOG_SIZE: number
14
+ EPS?: number
15
+ }) => {
16
+ if (si !== 0 && si !== pts.length - 2) return
17
+
18
+ const start = pts[si]!
19
+ const end = pts[si + 1]!
20
+ const isVertical = Math.abs(start.x - end.x) < EPS
21
+ const isHorizontal = Math.abs(start.y - end.y) < EPS
22
+ if (!isVertical && !isHorizontal) return
23
+
24
+ const segDir = isVertical
25
+ ? end.y > start.y
26
+ ? 1
27
+ : -1
28
+ : end.x > start.x
29
+ ? 1
30
+ : -1
31
+
32
+ if (si === 0) {
33
+ if (isVertical) {
34
+ const jogY = start.y + segDir * JOG_SIZE
35
+ pts.splice(
36
+ 1,
37
+ 1,
38
+ { x: start.x, y: jogY },
39
+ { x: start.x + offset, y: jogY },
40
+ { x: end.x + offset, y: end.y },
41
+ )
42
+ } else {
43
+ // Horizontal
44
+ const jogX = start.x + segDir * JOG_SIZE
45
+ pts.splice(
46
+ 1,
47
+ 1,
48
+ { x: jogX, y: start.y },
49
+ { x: jogX, y: start.y + offset },
50
+ { x: end.x, y: end.y + offset },
51
+ )
52
+ }
53
+ } else {
54
+ // si === pts.length - 2
55
+ if (isVertical) {
56
+ const jogY = end.y - segDir * JOG_SIZE
57
+ pts.splice(
58
+ si,
59
+ 1,
60
+ { x: start.x + offset, y: start.y },
61
+ { x: end.x + offset, y: jogY },
62
+ { x: end.x, y: jogY },
63
+ )
64
+ } else {
65
+ // Horizontal
66
+ const jogX = end.x - segDir * JOG_SIZE
67
+ pts.splice(
68
+ si,
69
+ 1,
70
+ { x: start.x, y: start.y + offset },
71
+ { x: jogX, y: end.y + offset },
72
+ { x: jogX, y: end.y },
73
+ )
74
+ }
75
+ }
76
+ }
@@ -89,7 +89,7 @@ export class TraceOverlapShiftSolver extends BaseSolver {
89
89
  overlappingTraceSegments: Array<OverlappingTraceSegmentLocator>
90
90
  } | null {
91
91
  // Detect the next set of overlapping segments between two different net islands.
92
- const EPS = 1e-6
92
+ const EPS = 2e-3
93
93
 
94
94
  const netIds = Object.keys(this.traceNetIslands)
95
95
  // Compare each pair of different nets
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.30",
4
+ "version": "0.0.32",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",