@tscircuit/schematic-trace-solver 0.0.197 → 0.0.199

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 (22) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.js +84 -5
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +10 -1
  4. package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +128 -2
  5. package/lib/solvers/RailNetLabelCornerPlacementSolver/types.ts +1 -0
  6. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +2 -0
  7. package/package.json +1 -1
  8. package/site/SchematicTracePipelineSolver/repro-am3352-sheet3-power-rails.page.tsx +7 -0
  9. package/site/bug-reports/bug-report-20260911T195723Z.page.tsx +4 -0
  10. package/site/bug-reports/bug-report-20260916T054012Z.page.tsx +4 -0
  11. package/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg +559 -0
  12. package/tests/bug-reports/bug-report-20260911T195723Z/bug-report-20260911T195723Z.json +1792 -0
  13. package/tests/bug-reports/bug-report-20260911T195723Z/bug-report-20260911T195723Z.test.ts +35 -0
  14. package/tests/bug-reports/bug-report-20260916T054012Z/__snapshots__/bug-report-20260916T054012Z.snap.svg +504 -0
  15. package/tests/bug-reports/bug-report-20260916T054012Z/bug-report-20260916T054012Z.json +1847 -0
  16. package/tests/bug-reports/bug-report-20260916T054012Z/bug-report-20260916T054012Z.test.ts +28 -0
  17. package/tests/repros/__snapshots__/repro-am3352-sheet3-power-rails.snap.svg +187 -0
  18. package/tests/repros/__snapshots__/repro-normal-net-label-branch-from-junction.snap.svg +31 -31
  19. package/tests/repros/assets/repro-am3352-sheet3-power-rails.input.json +698 -0
  20. package/tests/repros/repro-am3352-sheet3-power-rails.md +66 -0
  21. package/tests/repros/repro-am3352-sheet3-power-rails.test.ts +39 -0
  22. package/tests/repros/repro-normal-net-label-branch-from-junction.test.ts +13 -0
package/dist/index.d.ts CHANGED
@@ -963,6 +963,7 @@ interface RailNetLabelCornerPlacementSolverParams {
963
963
  inputProblem: InputProblem;
964
964
  traces: SolvedTracePath[];
965
965
  netLabelPlacements: NetLabelPlacement[];
966
+ netLabelConnectorTraceIds?: ReadonlySet<string>;
966
967
  }
967
968
  type TraceCornerCandidate = {
968
969
  anchorPoint: Point;
@@ -996,6 +997,7 @@ declare class RailNetLabelCornerPlacementSolver extends BaseSolver {
996
997
  private queuedCornerCandidates;
997
998
  private shouldAdvanceToNextLabel;
998
999
  private traceMap;
1000
+ private netLabelConnectorTraceIds;
999
1001
  constructor(params: RailNetLabelCornerPlacementSolverParams);
1000
1002
  getConstructorParams(): ConstructorParameters<typeof RailNetLabelCornerPlacementSolver>[0];
1001
1003
  _step(): void;
@@ -1015,6 +1017,8 @@ declare class RailNetLabelCornerPlacementSolver extends BaseSolver {
1015
1017
  private intersectsAnyChip;
1016
1018
  private intersectsAnyOtherNetLabel;
1017
1019
  private getCornerCandidatesForLabel;
1020
+ private getStraightConnectorCandidates;
1021
+ private isComponentAdjacentCorner;
1018
1022
  private isConfiguredRailLabel;
1019
1023
  private isLabelCrossedByOtherNetTrace;
1020
1024
  private pointsEqual;
package/dist/index.js CHANGED
@@ -10274,7 +10274,8 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10274
10274
  result.selected = true;
10275
10275
  return result;
10276
10276
  }
10277
- if (stopOnTraceCollision && result.status === "trace-collision") break;
10277
+ if (stopOnTraceCollision && result.status === "trace-collision" && (!projectedConnectorSource || orientation !== "y-" || !this.isGroundLabel(label)))
10278
+ break;
10278
10279
  }
10279
10280
  return null;
10280
10281
  }
@@ -11181,6 +11182,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11181
11182
  queuedCornerCandidates = [];
11182
11183
  shouldAdvanceToNextLabel = false;
11183
11184
  traceMap;
11185
+ netLabelConnectorTraceIds;
11184
11186
  constructor(params) {
11185
11187
  super();
11186
11188
  this.inputProblem = params.inputProblem;
@@ -11190,6 +11192,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11190
11192
  this.traceMap = Object.fromEntries(
11191
11193
  params.traces.map((trace) => [trace.mspPairId, trace])
11192
11194
  );
11195
+ this.netLabelConnectorTraceIds = params.netLabelConnectorTraceIds ?? /* @__PURE__ */ new Set();
11193
11196
  this.queuedLabelIndices = this.getProcessableLabelIndices();
11194
11197
  this.prepareNextLabel();
11195
11198
  }
@@ -11197,7 +11200,8 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11197
11200
  return {
11198
11201
  inputProblem: this.inputProblem,
11199
11202
  traces: this.traces,
11200
- netLabelPlacements: this.netLabelPlacements
11203
+ netLabelPlacements: this.netLabelPlacements,
11204
+ netLabelConnectorTraceIds: this.netLabelConnectorTraceIds
11201
11205
  };
11202
11206
  }
11203
11207
  _step() {
@@ -11248,7 +11252,10 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11248
11252
  this.currentLabelIndex = labelIndex;
11249
11253
  this.currentLabel = label;
11250
11254
  this.currentCandidateResults = [];
11251
- this.queuedCornerCandidates = this.getCornerCandidatesForLabel(label);
11255
+ this.queuedCornerCandidates = [
11256
+ ...this.getStraightConnectorCandidates(label),
11257
+ ...this.getCornerCandidatesForLabel(label)
11258
+ ];
11252
11259
  return true;
11253
11260
  }
11254
11261
  advanceToNextLabel() {
@@ -11330,7 +11337,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11330
11337
  }
11331
11338
  shouldProcessLabel(label) {
11332
11339
  const isVerticalRailLabel = label.orientation === "y+" || label.orientation === "y-";
11333
- return isVerticalRailLabel && this.getCornerCandidatesForLabel(label).length > 0;
11340
+ return isVerticalRailLabel && (this.getStraightConnectorCandidates(label).length > 0 || this.getCornerCandidatesForLabel(label).length > 0);
11334
11341
  }
11335
11342
  intersectsAnyChip(bounds) {
11336
11343
  return this.inputProblem.chips.some(
@@ -11387,6 +11394,77 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
11387
11394
  ...railAlignedCandidates.sort((a, b) => a.distance - b.distance)
11388
11395
  ];
11389
11396
  }
11397
+ getStraightConnectorCandidates(label) {
11398
+ if (label.pinIds.length !== 2 || !this.isConfiguredRailLabel(label)) {
11399
+ return [];
11400
+ }
11401
+ const pins = this.inputProblem.chips.flatMap((chip) => chip.pins).filter((pin) => label.pinIds.includes(pin.pinId));
11402
+ if (pins.length !== 2 || pins[0]._facingDirection !== pins[1]._facingDirection || pins[0]._facingDirection !== "x+" && pins[0]._facingDirection !== "x-" || Math.abs(pins[0].y - pins[1].y) > EPS8) {
11403
+ return [];
11404
+ }
11405
+ const configuredOrientations = this.inputProblem.availableNetLabelOrientations[label.netId];
11406
+ if (configuredOrientations?.length !== 1 || configuredOrientations[0] !== label.orientation) {
11407
+ return [];
11408
+ }
11409
+ const netConnection = this.inputProblem.netConnections.find(
11410
+ (connection) => connection.netId === label.netId
11411
+ );
11412
+ if (netConnection?.isGround) return [];
11413
+ const connector = this.traces.find(
11414
+ (trace) => this.netLabelConnectorTraceIds.has(trace.mspPairId) && trace.globalConnNetId === label.globalConnNetId && label.pinIds.every((pinId) => trace.pinIds.includes(pinId)) && trace.tracePath.length >= 2 && (this.pointsEqual(trace.tracePath[0], label.anchorPoint) || this.pointsEqual(trace.tracePath.at(-1), label.anchorPoint))
11415
+ );
11416
+ if (!connector) return [];
11417
+ const sourcePoint = this.pointsEqual(
11418
+ connector.tracePath[0],
11419
+ label.anchorPoint
11420
+ ) ? connector.tracePath.at(-1) : connector.tracePath[0];
11421
+ if (!this.isComponentAdjacentCorner(label, sourcePoint)) return [];
11422
+ const direction = label.orientation === "y+" ? 1 : -1;
11423
+ const maxSearchDistance = Math.min(
11424
+ getMaxSearchDistance(this.inputProblem),
11425
+ label.height * 2
11426
+ );
11427
+ for (let distance7 = WICK_CLEARANCE + LABEL_SEARCH_STEP; distance7 <= maxSearchDistance + EPS8; distance7 += LABEL_SEARCH_STEP) {
11428
+ const anchorPoint = {
11429
+ x: sourcePoint.x,
11430
+ y: sourcePoint.y + direction * distance7
11431
+ };
11432
+ const center = getCenterFromAnchor(
11433
+ anchorPoint,
11434
+ label.orientation,
11435
+ label.width,
11436
+ label.height
11437
+ );
11438
+ if (this.intersectsAnyChip(getRectBounds(center, label.width, label.height))) {
11439
+ continue;
11440
+ }
11441
+ return [
11442
+ {
11443
+ anchorPoint,
11444
+ traceId: connector.mspPairId,
11445
+ distance: getDistance(anchorPoint, label.anchorPoint),
11446
+ pinAligned: true,
11447
+ reroutedTracePath: [sourcePoint, anchorPoint]
11448
+ }
11449
+ ];
11450
+ }
11451
+ return [];
11452
+ }
11453
+ isComponentAdjacentCorner(label, corner) {
11454
+ return label.mspConnectionPairIds.some((traceId) => {
11455
+ const trace = this.traceMap[traceId];
11456
+ if (!trace || trace.tracePath.length < 2) return false;
11457
+ return [
11458
+ { point: trace.tracePath[0], neighbor: trace.tracePath[1] },
11459
+ {
11460
+ point: trace.tracePath.at(-1),
11461
+ neighbor: trace.tracePath.at(-2)
11462
+ }
11463
+ ].some(
11464
+ ({ point, neighbor }) => this.pointsEqual(neighbor, corner) && trace.pins.some((pin) => this.pointsEqual(pin, point))
11465
+ );
11466
+ });
11467
+ }
11390
11468
  isConfiguredRailLabel(label) {
11391
11469
  if (!label.netId) return false;
11392
11470
  return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
@@ -18710,7 +18788,8 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
18710
18788
  traces: Object.values(
18711
18789
  instance.postLabelTraceOverlapShiftSolver.correctedTraceMap
18712
18790
  ),
18713
- netLabelPlacements: instance.availableNetOrientationSolver.outputNetLabelPlacements
18791
+ netLabelPlacements: instance.availableNetOrientationSolver.outputNetLabelPlacements,
18792
+ netLabelConnectorTraceIds: instance.availableNetOrientationSolver.netLabelConnectorTraceIds
18714
18793
  }
18715
18794
  ];
18716
18795
  }
@@ -1172,7 +1172,16 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1172
1172
  result.selected = true
1173
1173
  return result
1174
1174
  }
1175
- if (stopOnTraceCollision && result.status === "trace-collision") break
1175
+ // A downward ground branch projected onto its existing trace can reach a
1176
+ // clear position below a crossing trace.
1177
+ if (
1178
+ stopOnTraceCollision &&
1179
+ result.status === "trace-collision" &&
1180
+ (!projectedConnectorSource ||
1181
+ orientation !== "y-" ||
1182
+ !this.isGroundLabel(label))
1183
+ )
1184
+ break
1176
1185
  }
1177
1186
 
1178
1187
  return null
@@ -1,9 +1,14 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
2
  import type { Point } from "@tscircuit/math-utils"
3
3
  import {
4
+ getMaxSearchDistance,
4
5
  segmentCrossesBoundsInterior,
5
6
  traceCrossesBoundsInterior,
6
7
  } from "lib/solvers/AvailableNetOrientationSolver/geometry"
8
+ import {
9
+ LABEL_SEARCH_STEP,
10
+ WICK_CLEARANCE,
11
+ } from "lib/solvers/AvailableNetOrientationSolver/constants"
7
12
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
8
13
  import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
9
14
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
@@ -51,6 +56,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
51
56
  private queuedCornerCandidates: TraceCornerCandidate[] = []
52
57
  private shouldAdvanceToNextLabel = false
53
58
  private traceMap: Record<string, SolvedTracePath>
59
+ private netLabelConnectorTraceIds: ReadonlySet<string>
54
60
 
55
61
  constructor(params: RailNetLabelCornerPlacementSolverParams) {
56
62
  super()
@@ -61,6 +67,8 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
61
67
  this.traceMap = Object.fromEntries(
62
68
  params.traces.map((trace) => [trace.mspPairId, trace]),
63
69
  )
70
+ this.netLabelConnectorTraceIds =
71
+ params.netLabelConnectorTraceIds ?? new Set()
64
72
  this.queuedLabelIndices = this.getProcessableLabelIndices()
65
73
  this.prepareNextLabel()
66
74
  }
@@ -72,6 +80,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
72
80
  inputProblem: this.inputProblem,
73
81
  traces: this.traces,
74
82
  netLabelPlacements: this.netLabelPlacements,
83
+ netLabelConnectorTraceIds: this.netLabelConnectorTraceIds,
75
84
  }
76
85
  }
77
86
 
@@ -135,7 +144,10 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
135
144
  this.currentLabelIndex = labelIndex
136
145
  this.currentLabel = label
137
146
  this.currentCandidateResults = []
138
- this.queuedCornerCandidates = this.getCornerCandidatesForLabel(label)
147
+ this.queuedCornerCandidates = [
148
+ ...this.getStraightConnectorCandidates(label),
149
+ ...this.getCornerCandidatesForLabel(label),
150
+ ]
139
151
 
140
152
  return true
141
153
  }
@@ -250,7 +262,9 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
250
262
  const isVerticalRailLabel =
251
263
  label.orientation === "y+" || label.orientation === "y-"
252
264
  return (
253
- isVerticalRailLabel && this.getCornerCandidatesForLabel(label).length > 0
265
+ isVerticalRailLabel &&
266
+ (this.getStraightConnectorCandidates(label).length > 0 ||
267
+ this.getCornerCandidatesForLabel(label).length > 0)
254
268
  )
255
269
  }
256
270
 
@@ -323,6 +337,118 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
323
337
  ]
324
338
  }
325
339
 
340
+ private getStraightConnectorCandidates(
341
+ label: NetLabelPlacement,
342
+ ): TraceCornerCandidate[] {
343
+ if (label.pinIds.length !== 2 || !this.isConfiguredRailLabel(label)) {
344
+ return []
345
+ }
346
+
347
+ const pins = this.inputProblem.chips
348
+ .flatMap((chip) => chip.pins)
349
+ .filter((pin) => label.pinIds.includes(pin.pinId))
350
+ if (
351
+ pins.length !== 2 ||
352
+ pins[0]!._facingDirection !== pins[1]!._facingDirection ||
353
+ (pins[0]!._facingDirection !== "x+" &&
354
+ pins[0]!._facingDirection !== "x-") ||
355
+ Math.abs(pins[0]!.y - pins[1]!.y) > EPS
356
+ ) {
357
+ return []
358
+ }
359
+
360
+ const configuredOrientations =
361
+ this.inputProblem.availableNetLabelOrientations[label.netId!]
362
+ if (
363
+ configuredOrientations?.length !== 1 ||
364
+ configuredOrientations[0] !== label.orientation
365
+ ) {
366
+ return []
367
+ }
368
+
369
+ const netConnection = this.inputProblem.netConnections.find(
370
+ (connection) => connection.netId === label.netId,
371
+ )
372
+ if (netConnection?.isGround) return []
373
+
374
+ const connector = this.traces.find(
375
+ (trace) =>
376
+ this.netLabelConnectorTraceIds.has(trace.mspPairId) &&
377
+ trace.globalConnNetId === label.globalConnNetId &&
378
+ label.pinIds.every((pinId) => trace.pinIds.includes(pinId)) &&
379
+ trace.tracePath.length >= 2 &&
380
+ (this.pointsEqual(trace.tracePath[0]!, label.anchorPoint) ||
381
+ this.pointsEqual(trace.tracePath.at(-1)!, label.anchorPoint)),
382
+ )
383
+ if (!connector) return []
384
+
385
+ const sourcePoint = this.pointsEqual(
386
+ connector.tracePath[0]!,
387
+ label.anchorPoint,
388
+ )
389
+ ? connector.tracePath.at(-1)!
390
+ : connector.tracePath[0]!
391
+ if (!this.isComponentAdjacentCorner(label, sourcePoint)) return []
392
+
393
+ const direction = label.orientation === "y+" ? 1 : -1
394
+ const maxSearchDistance = Math.min(
395
+ getMaxSearchDistance(this.inputProblem),
396
+ label.height * 2,
397
+ )
398
+ for (
399
+ let distance = WICK_CLEARANCE + LABEL_SEARCH_STEP;
400
+ distance <= maxSearchDistance + EPS;
401
+ distance += LABEL_SEARCH_STEP
402
+ ) {
403
+ const anchorPoint = {
404
+ x: sourcePoint.x,
405
+ y: sourcePoint.y + direction * distance,
406
+ }
407
+ const center = getCenterFromAnchor(
408
+ anchorPoint,
409
+ label.orientation,
410
+ label.width,
411
+ label.height,
412
+ )
413
+ if (
414
+ this.intersectsAnyChip(getRectBounds(center, label.width, label.height))
415
+ ) {
416
+ continue
417
+ }
418
+
419
+ return [
420
+ {
421
+ anchorPoint,
422
+ traceId: connector.mspPairId,
423
+ distance: getDistance(anchorPoint, label.anchorPoint),
424
+ pinAligned: true,
425
+ reroutedTracePath: [sourcePoint, anchorPoint],
426
+ },
427
+ ]
428
+ }
429
+
430
+ return []
431
+ }
432
+
433
+ private isComponentAdjacentCorner(label: NetLabelPlacement, corner: Point) {
434
+ return label.mspConnectionPairIds.some((traceId) => {
435
+ const trace = this.traceMap[traceId]
436
+ if (!trace || trace.tracePath.length < 2) return false
437
+
438
+ return [
439
+ { point: trace.tracePath[0]!, neighbor: trace.tracePath[1]! },
440
+ {
441
+ point: trace.tracePath.at(-1)!,
442
+ neighbor: trace.tracePath.at(-2)!,
443
+ },
444
+ ].some(
445
+ ({ point, neighbor }) =>
446
+ this.pointsEqual(neighbor, corner) &&
447
+ trace.pins.some((pin) => this.pointsEqual(pin, point)),
448
+ )
449
+ })
450
+ }
451
+
326
452
  private isConfiguredRailLabel(label: NetLabelPlacement) {
327
453
  if (!label.netId) return false
328
454
  return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
@@ -7,6 +7,7 @@ export interface RailNetLabelCornerPlacementSolverParams {
7
7
  inputProblem: InputProblem
8
8
  traces: SolvedTracePath[]
9
9
  netLabelPlacements: NetLabelPlacement[]
10
+ netLabelConnectorTraceIds?: ReadonlySet<string>
10
11
  }
11
12
 
12
13
  export type Bounds = {
@@ -402,6 +402,8 @@ export class SchematicTracePipelineSolver extends BaseSolver {
402
402
  ),
403
403
  netLabelPlacements:
404
404
  instance.availableNetOrientationSolver!.outputNetLabelPlacements,
405
+ netLabelConnectorTraceIds:
406
+ instance.availableNetOrientationSolver!.netLabelConnectorTraceIds,
405
407
  },
406
408
  ]
407
409
  },
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.197",
8
+ "version": "0.0.199",
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-am3352-sheet3-power-rails.input.json"
4
+
5
+ export default () => (
6
+ <PipelineDebugger inputProblem={structuredClone(input) as InputProblem} />
7
+ )
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260911T195723Z/bug-report-20260911T195723Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260916T054012Z/bug-report-20260916T054012Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />