@tscircuit/schematic-trace-solver 0.0.88 → 0.0.90

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 (23) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.js +112 -29
  3. package/lib/solvers/Example28Solver/reroute.ts +10 -0
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +116 -5
  5. package/lib/testing/PipelineStageDebugRunner.ts +21 -4
  6. package/package.json +1 -1
  7. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +386 -0
  8. package/tests/bug-reports/bug-report-20260706T213649Z/bug-report-20260706T213649Z.json +1471 -0
  9. package/tests/bug-reports/bug-report-20260706T213649Z/bug-report-20260706T213649Z.test.ts +12 -0
  10. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +22 -22
  11. package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +20 -22
  12. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +251 -2097
  13. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +123 -856
  14. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +72 -274
  15. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +153 -949
  16. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +66 -217
  17. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +111 -612
  18. package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.test.ts +11 -0
  19. package/tests/examples/__snapshots__/example03.snap.svg +44 -44
  20. package/tests/examples/__snapshots__/example32.snap.svg +3 -3
  21. package/tests/examples/__snapshots__/example42.snap.svg +1 -1
  22. package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +87 -283
  23. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +117 -469
package/dist/index.d.ts CHANGED
@@ -170,6 +170,7 @@ declare class SchematicTraceSingleLineSolver2 extends BaseSolver {
170
170
  chipMap: Record<string, InputChip>;
171
171
  obstacles: ObstacleRect[];
172
172
  textObstacles: Set<ObstacleRect>;
173
+ endpointTextObstacles: Set<ObstacleRect>;
173
174
  aabb: {
174
175
  minX: number;
175
176
  maxX: number;
@@ -192,6 +193,9 @@ declare class SchematicTraceSingleLineSolver2 extends BaseSolver {
192
193
  private getNetLabelHeightForConnectionPair;
193
194
  private axisOfSegment;
194
195
  private pathLength;
196
+ private getPinBandPenalty;
197
+ private pathCost;
198
+ private isSegmentOutsidePinBand;
195
199
  _step(): void;
196
200
  visualize(): GraphicsObject;
197
201
  }
package/dist/index.js CHANGED
@@ -854,6 +854,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
854
854
  chipMap;
855
855
  obstacles;
856
856
  textObstacles;
857
+ endpointTextObstacles;
857
858
  aabb;
858
859
  baseElbow;
859
860
  solvedTracePath = null;
@@ -877,6 +878,12 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
877
878
  this.textObstacles = new Set(
878
879
  this.obstacles.filter((r) => r.kind === "text_box")
879
880
  );
881
+ const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId));
882
+ this.endpointTextObstacles = new Set(
883
+ endpointChipIds.size > 1 ? this.obstacles.filter(
884
+ (r) => r.kind === "text_box" && r.textBox.chipId !== void 0 && endpointChipIds.has(r.textBox.chipId)
885
+ ) : []
886
+ );
880
887
  const [pin1, pin2] = this.pins;
881
888
  this.baseElbow = calculateElbow(
882
889
  {
@@ -985,6 +992,31 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
985
992
  }
986
993
  return sum;
987
994
  }
995
+ getPinBandPenalty(path) {
996
+ let penalty = 0;
997
+ for (let i = 1; i < path.length - 2; i++) {
998
+ const a = path[i];
999
+ const b = path[i + 1];
1000
+ if (isHorizontal(a, b) && a.y > this.aabb.minY && a.y < this.aabb.maxY) {
1001
+ penalty += 10;
1002
+ } else if (isVertical(a, b) && a.x > this.aabb.minX && a.x < this.aabb.maxX) {
1003
+ penalty += 10;
1004
+ }
1005
+ }
1006
+ return penalty;
1007
+ }
1008
+ pathCost(path) {
1009
+ return this.pathLength(path) + this.getPinBandPenalty(path);
1010
+ }
1011
+ isSegmentOutsidePinBand(a, b) {
1012
+ if (isHorizontal(a, b)) {
1013
+ return a.y <= this.aabb.minY || a.y >= this.aabb.maxY;
1014
+ }
1015
+ if (isVertical(a, b)) {
1016
+ return a.x <= this.aabb.minX || a.x >= this.aabb.maxX;
1017
+ }
1018
+ return false;
1019
+ }
988
1020
  _step() {
989
1021
  if (this.solvedTracePath) {
990
1022
  this.solved = true;
@@ -1000,10 +1032,17 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1000
1032
  const [PA, PB] = this.pins;
1001
1033
  const collision = findFirstCollision(path, this.obstacles, {
1002
1034
  excludeRectsForSegment: (segIndex2) => {
1003
- const lastSegIndex = path.length - 2;
1004
- if (segIndex2 === 0 || segIndex2 === lastSegIndex) {
1035
+ const lastSegIndex2 = path.length - 2;
1036
+ if (segIndex2 === 0 || segIndex2 === lastSegIndex2) {
1005
1037
  return this.textObstacles;
1006
1038
  }
1039
+ const adjacentEndpointSegIndex = segIndex2 === 1 ? 2 : segIndex2 === lastSegIndex2 - 1 ? lastSegIndex2 - 2 : null;
1040
+ if (adjacentEndpointSegIndex !== null && adjacentEndpointSegIndex >= 0 && adjacentEndpointSegIndex < lastSegIndex2 && this.isSegmentOutsidePinBand(
1041
+ path[adjacentEndpointSegIndex],
1042
+ path[adjacentEndpointSegIndex + 1]
1043
+ )) {
1044
+ return this.endpointTextObstacles;
1045
+ }
1007
1046
  return /* @__PURE__ */ new Set();
1008
1047
  }
1009
1048
  });
@@ -1018,6 +1057,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1018
1057
  }
1019
1058
  return;
1020
1059
  }
1060
+ const originalSegIndex = collision.segIndex;
1021
1061
  let { segIndex, rect } = collision;
1022
1062
  const isFirstSegment = segIndex === 0;
1023
1063
  const isLastSegment = segIndex === path.length - 2;
@@ -1050,16 +1090,51 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1050
1090
  candidates.push(...mids);
1051
1091
  }
1052
1092
  const newStates = [];
1053
- for (const coord of candidates) {
1054
- const newPath = shiftSegmentOrth(path, segIndex, axis, coord);
1055
- if (!newPath) continue;
1093
+ const addShiftedCandidate = (candidateSegIndex, candidateAxis, coord) => {
1094
+ const newPath = shiftSegmentOrth(
1095
+ path,
1096
+ candidateSegIndex,
1097
+ candidateAxis,
1098
+ coord
1099
+ );
1100
+ if (!newPath) return;
1056
1101
  const key = pathKey(newPath);
1057
- if (this.visited.has(key)) continue;
1102
+ if (this.visited.has(key)) return;
1058
1103
  this.visited.add(key);
1059
1104
  const nextSet = new Set(collisionRects);
1060
1105
  nextSet.add(rect);
1061
- const len = this.pathLength(newPath);
1106
+ const len = this.pathCost(newPath);
1062
1107
  newStates.push({ path: newPath, collisionRects: nextSet, len });
1108
+ };
1109
+ for (const coord of candidates) {
1110
+ addShiftedCandidate(segIndex, axis, coord);
1111
+ }
1112
+ const lastSegIndex = path.length - 2;
1113
+ const adjacentSegmentIndexes = originalSegIndex === 1 ? [2] : originalSegIndex === lastSegIndex - 1 ? [lastSegIndex - 2] : [];
1114
+ for (const adjacentSegIndex of adjacentSegmentIndexes) {
1115
+ if (adjacentSegIndex < 0 || adjacentSegIndex >= lastSegIndex) continue;
1116
+ const adjacentAxis = this.axisOfSegment(
1117
+ path[adjacentSegIndex],
1118
+ path[adjacentSegIndex + 1]
1119
+ );
1120
+ if (!adjacentAxis || adjacentAxis === axis) continue;
1121
+ const adjacentCandidates = [
1122
+ ...midBetweenPointAndRect(adjacentAxis, { x: PA.x, y: PA.y }, rect),
1123
+ ...midBetweenPointAndRect(adjacentAxis, { x: PB.x, y: PB.y }, rect)
1124
+ ];
1125
+ if (collisionRects.size > 0) {
1126
+ adjacentCandidates.push(
1127
+ ...candidateMidsFromSet(
1128
+ adjacentAxis,
1129
+ rect,
1130
+ collisionRects,
1131
+ this.aabb
1132
+ )
1133
+ );
1134
+ }
1135
+ for (const coord of [...new Set(adjacentCandidates)]) {
1136
+ addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord);
1137
+ }
1063
1138
  }
1064
1139
  newStates.sort((a2, b2) => a2.len - b2.len);
1065
1140
  for (const st of newStates) {
@@ -5419,6 +5494,28 @@ var moveAttachedLabelsToReroutedTrace = ({
5419
5494
  };
5420
5495
  });
5421
5496
 
5497
+ // lib/solvers/Example28Solver/doesPathRunAlongChipBoundary.ts
5498
+ var doesPathRunAlongChipBoundary = (path, chipObstacles) => {
5499
+ for (let i = 0; i < path.length - 1; i++) {
5500
+ const start = path[i];
5501
+ const end = path[i + 1];
5502
+ for (const obstacle of chipObstacles) {
5503
+ if (!segmentRunsAlongRectBoundary(start, end, obstacle)) continue;
5504
+ if (getSegmentOverlapWithRectSpan(start, end, obstacle) > EPS7) {
5505
+ return true;
5506
+ }
5507
+ }
5508
+ }
5509
+ return false;
5510
+ };
5511
+ var getSegmentOverlapWithRectSpan = (start, end, rect) => {
5512
+ const isVertical4 = Math.abs(start.x - end.x) < EPS7;
5513
+ if (isVertical4) {
5514
+ return Math.min(Math.max(start.y, end.y), rect.maxY) - Math.max(Math.min(start.y, end.y), rect.minY);
5515
+ }
5516
+ return Math.min(Math.max(start.x, end.x), rect.maxX) - Math.max(Math.min(start.x, end.x), rect.minX);
5517
+ };
5518
+
5422
5519
  // lib/solvers/Example28Solver/reroute.ts
5423
5520
  var LABEL_CLEARANCE = 0.1;
5424
5521
  var findBestReroutePath = ({
@@ -5529,6 +5626,14 @@ var createCandidateResult = ({
5529
5626
  selected: false
5530
5627
  };
5531
5628
  }
5629
+ if (doesPathRunAlongChipBoundary(path, chipObstacles)) {
5630
+ return {
5631
+ path,
5632
+ status: "chip-collision",
5633
+ usesHorizontalSegmentPush,
5634
+ selected: false
5635
+ };
5636
+ }
5532
5637
  return {
5533
5638
  path,
5534
5639
  score: scoreTracePath({
@@ -5792,28 +5897,6 @@ var countCrossingsWithOtherTraces = (params) => {
5792
5897
  return crossingCount;
5793
5898
  };
5794
5899
 
5795
- // lib/solvers/Example28Solver/doesPathRunAlongChipBoundary.ts
5796
- var doesPathRunAlongChipBoundary = (path, chipObstacles) => {
5797
- for (let i = 0; i < path.length - 1; i++) {
5798
- const start = path[i];
5799
- const end = path[i + 1];
5800
- for (const obstacle of chipObstacles) {
5801
- if (!segmentRunsAlongRectBoundary(start, end, obstacle)) continue;
5802
- if (getSegmentOverlapWithRectSpan(start, end, obstacle) > EPS7) {
5803
- return true;
5804
- }
5805
- }
5806
- }
5807
- return false;
5808
- };
5809
- var getSegmentOverlapWithRectSpan = (start, end, rect) => {
5810
- const isVertical4 = Math.abs(start.x - end.x) < EPS7;
5811
- if (isVertical4) {
5812
- return Math.min(Math.max(start.y, end.y), rect.maxY) - Math.max(Math.min(start.y, end.y), rect.minY);
5813
- }
5814
- return Math.min(Math.max(start.x, end.x), rect.maxX) - Math.max(Math.min(start.x, end.x), rect.minX);
5815
- };
5816
-
5817
5900
  // lib/solvers/Example28Solver/scoreSolutionTracePath.ts
5818
5901
  var scoreSolutionTracePath = (params) => {
5819
5902
  const { traces, outputTraces, chipObstacles } = params;
@@ -14,6 +14,7 @@ import {
14
14
  getPathLength,
15
15
  isPathCollidingWithChipInterior,
16
16
  } from "./geometry"
17
+ import { doesPathRunAlongChipBoundary } from "./doesPathRunAlongChipBoundary"
17
18
  import type {
18
19
  ChipObstacle,
19
20
  RerouteCandidateResult,
@@ -165,6 +166,15 @@ const createCandidateResult = ({
165
166
  }
166
167
  }
167
168
 
169
+ if (doesPathRunAlongChipBoundary(path, chipObstacles)) {
170
+ return {
171
+ path,
172
+ status: "chip-collision",
173
+ usesHorizontalSegmentPush,
174
+ selected: false,
175
+ }
176
+ }
177
+
168
178
  return {
169
179
  path,
170
180
  score: scoreTracePath({
@@ -29,6 +29,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
29
29
 
30
30
  obstacles: ObstacleRect[]
31
31
  textObstacles: Set<ObstacleRect>
32
+ endpointTextObstacles: Set<ObstacleRect>
32
33
  aabb: { minX: number; maxX: number; minY: number; maxY: number }
33
34
 
34
35
  baseElbow: Point[]
@@ -67,6 +68,17 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
67
68
  this.textObstacles = new Set(
68
69
  this.obstacles.filter((r) => r.kind === "text_box"),
69
70
  )
71
+ const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
72
+ this.endpointTextObstacles = new Set(
73
+ endpointChipIds.size > 1
74
+ ? this.obstacles.filter(
75
+ (r) =>
76
+ r.kind === "text_box" &&
77
+ r.textBox.chipId !== undefined &&
78
+ endpointChipIds.has(r.textBox.chipId),
79
+ )
80
+ : [],
81
+ )
70
82
 
71
83
  // Build initial elbow path
72
84
  const [pin1, pin2] = this.pins
@@ -203,6 +215,38 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
203
215
  return sum
204
216
  }
205
217
 
218
+ private getPinBandPenalty(path: Point[]): number {
219
+ let penalty = 0
220
+ for (let i = 1; i < path.length - 2; i++) {
221
+ const a = path[i]!
222
+ const b = path[i + 1]!
223
+ if (isHorizontal(a, b) && a.y > this.aabb.minY && a.y < this.aabb.maxY) {
224
+ penalty += 10
225
+ } else if (
226
+ isVertical(a, b) &&
227
+ a.x > this.aabb.minX &&
228
+ a.x < this.aabb.maxX
229
+ ) {
230
+ penalty += 10
231
+ }
232
+ }
233
+ return penalty
234
+ }
235
+
236
+ private pathCost(path: Point[]): number {
237
+ return this.pathLength(path) + this.getPinBandPenalty(path)
238
+ }
239
+
240
+ private isSegmentOutsidePinBand(a: Point, b: Point): boolean {
241
+ if (isHorizontal(a, b)) {
242
+ return a.y <= this.aabb.minY || a.y >= this.aabb.maxY
243
+ }
244
+ if (isVertical(a, b)) {
245
+ return a.x <= this.aabb.minX || a.x >= this.aabb.maxX
246
+ }
247
+ return false
248
+ }
249
+
206
250
  override _step() {
207
251
  if (this.solvedTracePath) {
208
252
  this.solved = true
@@ -225,6 +269,23 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
225
269
  if (segIndex === 0 || segIndex === lastSegIndex) {
226
270
  return this.textObstacles
227
271
  }
272
+ const adjacentEndpointSegIndex =
273
+ segIndex === 1
274
+ ? 2
275
+ : segIndex === lastSegIndex - 1
276
+ ? lastSegIndex - 2
277
+ : null
278
+ if (
279
+ adjacentEndpointSegIndex !== null &&
280
+ adjacentEndpointSegIndex >= 0 &&
281
+ adjacentEndpointSegIndex < lastSegIndex &&
282
+ this.isSegmentOutsidePinBand(
283
+ path[adjacentEndpointSegIndex]!,
284
+ path[adjacentEndpointSegIndex + 1]!,
285
+ )
286
+ ) {
287
+ return this.endpointTextObstacles
288
+ }
228
289
  return new Set<ObstacleRect>()
229
290
  },
230
291
  })
@@ -246,6 +307,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
246
307
  return
247
308
  }
248
309
 
310
+ const originalSegIndex = collision.segIndex
249
311
  let { segIndex, rect } = collision
250
312
 
251
313
  // Never move the first or last segments - move adjacent segment instead
@@ -301,18 +363,67 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
301
363
  len: number
302
364
  }> = []
303
365
 
304
- for (const coord of candidates) {
305
- const newPath = shiftSegmentOrth(path, segIndex, axis, coord)
306
- if (!newPath) continue
366
+ const addShiftedCandidate = (
367
+ candidateSegIndex: number,
368
+ candidateAxis: Axis,
369
+ coord: number,
370
+ ) => {
371
+ const newPath = shiftSegmentOrth(
372
+ path,
373
+ candidateSegIndex,
374
+ candidateAxis,
375
+ coord,
376
+ )
377
+ if (!newPath) return
307
378
  const key = pathKey(newPath)
308
- if (this.visited.has(key)) continue
379
+ if (this.visited.has(key)) return
309
380
  this.visited.add(key)
310
381
  const nextSet = new Set(collisionRects)
311
382
  nextSet.add(rect)
312
- const len = this.pathLength(newPath)
383
+ const len = this.pathCost(newPath)
313
384
  newStates.push({ path: newPath, collisionRects: nextSet, len })
314
385
  }
315
386
 
387
+ for (const coord of candidates) {
388
+ addShiftedCandidate(segIndex, axis, coord)
389
+ }
390
+
391
+ const lastSegIndex = path.length - 2
392
+ const adjacentSegmentIndexes =
393
+ originalSegIndex === 1
394
+ ? [2]
395
+ : originalSegIndex === lastSegIndex - 1
396
+ ? [lastSegIndex - 2]
397
+ : []
398
+
399
+ for (const adjacentSegIndex of adjacentSegmentIndexes) {
400
+ if (adjacentSegIndex < 0 || adjacentSegIndex >= lastSegIndex) continue
401
+ const adjacentAxis = this.axisOfSegment(
402
+ path[adjacentSegIndex]!,
403
+ path[adjacentSegIndex + 1]!,
404
+ )
405
+ if (!adjacentAxis || adjacentAxis === axis) continue
406
+
407
+ const adjacentCandidates = [
408
+ ...midBetweenPointAndRect(adjacentAxis, { x: PA.x, y: PA.y }, rect),
409
+ ...midBetweenPointAndRect(adjacentAxis, { x: PB.x, y: PB.y }, rect),
410
+ ]
411
+ if (collisionRects.size > 0) {
412
+ adjacentCandidates.push(
413
+ ...candidateMidsFromSet(
414
+ adjacentAxis,
415
+ rect,
416
+ collisionRects,
417
+ this.aabb,
418
+ ),
419
+ )
420
+ }
421
+
422
+ for (const coord of [...new Set(adjacentCandidates)]) {
423
+ addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord)
424
+ }
425
+ }
426
+
316
427
  newStates.sort((a, b) => a.len - b.len)
317
428
  for (const st of newStates) {
318
429
  this.queue.push({ path: st.path, collisionRects: st.collisionRects })
@@ -1,9 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises"
2
2
  import path from "node:path"
3
- import {
4
- getPngBufferFromGraphicsObject,
5
- getSvgFromGraphicsObject,
6
- } from "graphics-debug"
3
+ import * as graphicsDebug from "graphics-debug"
4
+ import { getSvgFromGraphicsObject } from "graphics-debug"
7
5
  import type { GraphicsObject } from "graphics-debug"
8
6
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
9
7
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
@@ -262,6 +260,25 @@ export class PipelineStageDebugRunner {
262
260
  }
263
261
 
264
262
  private renderGraphicsToPng(graphics: GraphicsObject): Promise<Uint8Array> {
263
+ const getPngBufferFromGraphicsObject = (
264
+ graphicsDebug as unknown as {
265
+ getPngBufferFromGraphicsObject?: (
266
+ graphics: GraphicsObject,
267
+ opts: {
268
+ backgroundColor: string
269
+ pngWidth: number
270
+ pngHeight: number
271
+ },
272
+ ) => Promise<Uint8Array>
273
+ }
274
+ ).getPngBufferFromGraphicsObject
275
+
276
+ if (!getPngBufferFromGraphicsObject) {
277
+ throw new Error(
278
+ "graphics-debug does not provide getPngBufferFromGraphicsObject; run with --no-png --svg or upgrade graphics-debug",
279
+ )
280
+ }
281
+
265
282
  return getPngBufferFromGraphicsObject(graphics, {
266
283
  backgroundColor: "white",
267
284
  pngWidth: this.pngWidth,
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.88",
4
+ "version": "0.0.90",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",