@tscircuit/schematic-trace-solver 0.0.92 → 0.0.93

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/dist/index.d.ts CHANGED
@@ -194,7 +194,6 @@ declare class SchematicTraceSingleLineSolver2 extends BaseSolver {
194
194
  private axisOfSegment;
195
195
  private pathLength;
196
196
  private getPinBandPenalty;
197
- private pathCost;
198
197
  private isSegmentOutsidePinBand;
199
198
  _step(): void;
200
199
  visualize(): GraphicsObject;
@@ -650,6 +649,13 @@ type TracePathScore = {
650
649
  labelIntersections: number;
651
650
  labelHugDistance: number;
652
651
  traceIntersections: number;
652
+ /**
653
+ * Total length the path runs along a chip boundary. A soft quality penalty
654
+ * (traces hugging a chip edge look bad) that is only preferred against once
655
+ * the more important factors above are equal, so a boundary-hugging route is
656
+ * still chosen over one that actually overlaps a label or another trace.
657
+ */
658
+ chipBoundaryOverlap: number;
653
659
  pathLength: number;
654
660
  };
655
661
  type RerouteCandidateResult = {
package/dist/index.js CHANGED
@@ -1112,9 +1112,6 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1112
1112
  }
1113
1113
  return penalty;
1114
1114
  }
1115
- pathCost(path) {
1116
- return this.pathLength(path) + this.getPinBandPenalty(path);
1117
- }
1118
1115
  isSegmentOutsidePinBand(a, b) {
1119
1116
  if (isHorizontal(a, b)) {
1120
1117
  return a.y <= this.aabb.minY || a.y >= this.aabb.maxY;
@@ -1210,8 +1207,12 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1210
1207
  this.visited.add(key);
1211
1208
  const nextSet = new Set(collisionRects);
1212
1209
  nextSet.add(rect);
1213
- const len = this.pathCost(newPath);
1214
- newStates.push({ path: newPath, collisionRects: nextSet, len });
1210
+ newStates.push({
1211
+ path: newPath,
1212
+ collisionRects: nextSet,
1213
+ length: this.pathLength(newPath),
1214
+ pinBandPenalty: this.getPinBandPenalty(newPath)
1215
+ });
1215
1216
  };
1216
1217
  for (const coord of candidates) {
1217
1218
  addShiftedCandidate(segIndex, axis, coord);
@@ -1243,7 +1244,9 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1243
1244
  addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord);
1244
1245
  }
1245
1246
  }
1246
- newStates.sort((a2, b2) => a2.len - b2.len);
1247
+ newStates.sort(
1248
+ (a2, b2) => a2.length - b2.length || a2.pinBandPenalty - b2.pinBandPenalty
1249
+ );
1247
1250
  for (const st of newStates) {
1248
1251
  this.queue.push({ path: st.path, collisionRects: st.collisionRects });
1249
1252
  }
@@ -5627,6 +5630,19 @@ var doesPathRunAlongChipBoundary = (path, chipObstacles) => {
5627
5630
  }
5628
5631
  return false;
5629
5632
  };
5633
+ var getChipBoundaryOverlap = (path, chipObstacles) => {
5634
+ let total = 0;
5635
+ for (let i = 0; i < path.length - 1; i++) {
5636
+ const start = path[i];
5637
+ const end = path[i + 1];
5638
+ for (const obstacle of chipObstacles) {
5639
+ if (!segmentRunsAlongRectBoundary(start, end, obstacle)) continue;
5640
+ const overlap = getSegmentOverlapWithRectSpan(start, end, obstacle);
5641
+ if (overlap > EPS7) total += overlap;
5642
+ }
5643
+ }
5644
+ return total;
5645
+ };
5630
5646
  var getSegmentOverlapWithRectSpan = (start, end, rect) => {
5631
5647
  const isVertical4 = Math.abs(start.x - end.x) < EPS7;
5632
5648
  if (isVertical4) {
@@ -5658,7 +5674,8 @@ var findBestReroutePath = ({
5658
5674
  obstacleLabel,
5659
5675
  outputTraces,
5660
5676
  outputNetLabelPlacements,
5661
- candidateResults
5677
+ candidateResults,
5678
+ chipObstacles
5662
5679
  });
5663
5680
  markSelectedCandidate(candidateResults, bestPath);
5664
5681
  return { bestPath, candidateResults };
@@ -5745,14 +5762,6 @@ var createCandidateResult = ({
5745
5762
  selected: false
5746
5763
  };
5747
5764
  }
5748
- if (doesPathRunAlongChipBoundary(path, chipObstacles)) {
5749
- return {
5750
- path,
5751
- status: "chip-collision",
5752
- usesHorizontalSegmentPush,
5753
- selected: false
5754
- };
5755
- }
5756
5765
  return {
5757
5766
  path,
5758
5767
  score: scoreTracePath({
@@ -5760,7 +5769,8 @@ var createCandidateResult = ({
5760
5769
  tracePath: path,
5761
5770
  obstacleLabel,
5762
5771
  outputTraces,
5763
- outputNetLabelPlacements
5772
+ outputNetLabelPlacements,
5773
+ chipObstacles
5764
5774
  }),
5765
5775
  status: "valid",
5766
5776
  usesHorizontalSegmentPush,
@@ -5850,7 +5860,8 @@ var selectBestReroutePath = ({
5850
5860
  obstacleLabel,
5851
5861
  outputTraces,
5852
5862
  outputNetLabelPlacements,
5853
- candidateResults
5863
+ candidateResults,
5864
+ chipObstacles
5854
5865
  }) => {
5855
5866
  for (const candidate of candidateResults) {
5856
5867
  if (!candidate.usesHorizontalSegmentPush) continue;
@@ -5864,7 +5875,8 @@ var selectBestReroutePath = ({
5864
5875
  tracePath: trace.tracePath,
5865
5876
  obstacleLabel,
5866
5877
  outputTraces,
5867
- outputNetLabelPlacements
5878
+ outputNetLabelPlacements,
5879
+ chipObstacles
5868
5880
  });
5869
5881
  for (const candidate of candidateResults) {
5870
5882
  if (candidate.status !== "valid" || !candidate.score) continue;
@@ -5950,7 +5962,8 @@ var scoreTracePath = ({
5950
5962
  tracePath,
5951
5963
  obstacleLabel,
5952
5964
  outputTraces,
5953
- outputNetLabelPlacements
5965
+ outputNetLabelPlacements,
5966
+ chipObstacles
5954
5967
  }) => {
5955
5968
  const candidateTrace = { ...trace, tracePath };
5956
5969
  return {
@@ -5960,6 +5973,7 @@ var scoreTracePath = ({
5960
5973
  }).length,
5961
5974
  labelHugDistance: getLabelHugDistance(tracePath, obstacleLabel),
5962
5975
  traceIntersections: countTraceIntersections(candidateTrace, outputTraces),
5976
+ chipBoundaryOverlap: getChipBoundaryOverlap(tracePath, chipObstacles),
5963
5977
  pathLength: getPathLength(tracePath)
5964
5978
  };
5965
5979
  };
@@ -5972,6 +5986,14 @@ var countTraceIntersections = (trace, outputTraces) => {
5972
5986
  return count;
5973
5987
  };
5974
5988
  var isBetterScore = (score, bestScore) => {
5989
+ const scoreHasLabelOverlap = score.labelIntersections > 0;
5990
+ const bestHasLabelOverlap = bestScore.labelIntersections > 0;
5991
+ if (scoreHasLabelOverlap !== bestHasLabelOverlap) {
5992
+ return !scoreHasLabelOverlap;
5993
+ }
5994
+ if (score.chipBoundaryOverlap !== bestScore.chipBoundaryOverlap) {
5995
+ return score.chipBoundaryOverlap < bestScore.chipBoundaryOverlap;
5996
+ }
5975
5997
  if (score.labelIntersections !== bestScore.labelIntersections) {
5976
5998
  return score.labelIntersections < bestScore.labelIntersections;
5977
5999
  }
@@ -20,6 +20,24 @@ export const doesPathRunAlongChipBoundary = (
20
20
  return false
21
21
  }
22
22
 
23
+ /** Total length of the path that runs along (overlapping) a chip boundary. */
24
+ export const getChipBoundaryOverlap = (
25
+ path: Point[],
26
+ chipObstacles: ChipObstacle[],
27
+ ) => {
28
+ let total = 0
29
+ for (let i = 0; i < path.length - 1; i++) {
30
+ const start = path[i]!
31
+ const end = path[i + 1]!
32
+ for (const obstacle of chipObstacles) {
33
+ if (!segmentRunsAlongRectBoundary(start, end, obstacle)) continue
34
+ const overlap = getSegmentOverlapWithRectSpan(start, end, obstacle)
35
+ if (overlap > EPS) total += overlap
36
+ }
37
+ }
38
+ return total
39
+ }
40
+
23
41
  const getSegmentOverlapWithRectSpan = (
24
42
  start: Point,
25
43
  end: Point,
@@ -14,7 +14,7 @@ import {
14
14
  getPathLength,
15
15
  isPathCollidingWithChipInterior,
16
16
  } from "./geometry"
17
- import { doesPathRunAlongChipBoundary } from "./doesPathRunAlongChipBoundary"
17
+ import { getChipBoundaryOverlap } from "./doesPathRunAlongChipBoundary"
18
18
  import type {
19
19
  ChipObstacle,
20
20
  RerouteCandidateResult,
@@ -52,6 +52,7 @@ export const findBestReroutePath = ({
52
52
  outputTraces,
53
53
  outputNetLabelPlacements,
54
54
  candidateResults,
55
+ chipObstacles,
55
56
  })
56
57
 
57
58
  markSelectedCandidate(candidateResults, bestPath)
@@ -166,15 +167,11 @@ const createCandidateResult = ({
166
167
  }
167
168
  }
168
169
 
169
- if (doesPathRunAlongChipBoundary(path, chipObstacles)) {
170
- return {
171
- path,
172
- status: "chip-collision",
173
- usesHorizontalSegmentPush,
174
- selected: false,
175
- }
176
- }
177
-
170
+ // Note: a path that merely runs along a chip boundary is NOT rejected here.
171
+ // It is a valid route, just a lower-quality one — scoreTracePath penalizes it
172
+ // via chipBoundaryOverlap so it loses only to routes that are otherwise as
173
+ // good. Hard-rejecting it discarded the best available route when every
174
+ // alternative overlapped a label or another trace.
178
175
  return {
179
176
  path,
180
177
  score: scoreTracePath({
@@ -183,6 +180,7 @@ const createCandidateResult = ({
183
180
  obstacleLabel,
184
181
  outputTraces,
185
182
  outputNetLabelPlacements,
183
+ chipObstacles,
186
184
  }),
187
185
  status: "valid",
188
186
  usesHorizontalSegmentPush,
@@ -293,12 +291,14 @@ const selectBestReroutePath = ({
293
291
  outputTraces,
294
292
  outputNetLabelPlacements,
295
293
  candidateResults,
294
+ chipObstacles,
296
295
  }: {
297
296
  trace: SolvedTracePath
298
297
  obstacleLabel: NetLabelPlacement
299
298
  outputTraces: SolvedTracePath[]
300
299
  outputNetLabelPlacements: NetLabelPlacement[]
301
300
  candidateResults: RerouteCandidateResult[]
301
+ chipObstacles: ChipObstacle[]
302
302
  }) => {
303
303
  for (const candidate of candidateResults) {
304
304
  if (!candidate.usesHorizontalSegmentPush) continue
@@ -314,6 +314,7 @@ const selectBestReroutePath = ({
314
314
  obstacleLabel,
315
315
  outputTraces,
316
316
  outputNetLabelPlacements,
317
+ chipObstacles,
317
318
  })
318
319
 
319
320
  for (const candidate of candidateResults) {
@@ -437,12 +438,14 @@ const scoreTracePath = ({
437
438
  obstacleLabel,
438
439
  outputTraces,
439
440
  outputNetLabelPlacements,
441
+ chipObstacles,
440
442
  }: {
441
443
  trace: SolvedTracePath
442
444
  tracePath: Point[]
443
445
  obstacleLabel: NetLabelPlacement
444
446
  outputTraces: SolvedTracePath[]
445
447
  outputNetLabelPlacements: NetLabelPlacement[]
448
+ chipObstacles: ChipObstacle[]
446
449
  }): TracePathScore => {
447
450
  const candidateTrace = { ...trace, tracePath }
448
451
  return {
@@ -452,6 +455,7 @@ const scoreTracePath = ({
452
455
  }).length,
453
456
  labelHugDistance: getLabelHugDistance(tracePath, obstacleLabel),
454
457
  traceIntersections: countTraceIntersections(candidateTrace, outputTraces),
458
+ chipBoundaryOverlap: getChipBoundaryOverlap(tracePath, chipObstacles),
455
459
  pathLength: getPathLength(tracePath),
456
460
  }
457
461
  }
@@ -469,6 +473,21 @@ const countTraceIntersections = (
469
473
  }
470
474
 
471
475
  const isBetterScore = (score: TracePathScore, bestScore: TracePathScore) => {
476
+ // A route with no label overlap is always preferred over one with any. This
477
+ // is separate from the full overlap count below so that a *clean* route wins
478
+ // even if it hugs a chip boundary, while a route that must overlap a label is
479
+ // still steered away from also hugging a boundary.
480
+ const scoreHasLabelOverlap = score.labelIntersections > 0
481
+ const bestHasLabelOverlap = bestScore.labelIntersections > 0
482
+ if (scoreHasLabelOverlap !== bestHasLabelOverlap) {
483
+ // Reached only when exactly one route overlaps a label; the clean one wins.
484
+ return !scoreHasLabelOverlap
485
+ }
486
+ // Boundary hugging is only accepted as the price of a clean route; once a
487
+ // label overlap is unavoidable, avoid hugging a boundary on top of it.
488
+ if (score.chipBoundaryOverlap !== bestScore.chipBoundaryOverlap) {
489
+ return score.chipBoundaryOverlap < bestScore.chipBoundaryOverlap
490
+ }
472
491
  if (score.labelIntersections !== bestScore.labelIntersections) {
473
492
  return score.labelIntersections < bestScore.labelIntersections
474
493
  }
@@ -15,6 +15,13 @@ export type TracePathScore = {
15
15
  labelIntersections: number
16
16
  labelHugDistance: number
17
17
  traceIntersections: number
18
+ /**
19
+ * Total length the path runs along a chip boundary. A soft quality penalty
20
+ * (traces hugging a chip edge look bad) that is only preferred against once
21
+ * the more important factors above are equal, so a boundary-hugging route is
22
+ * still chosen over one that actually overlaps a label or another trace.
23
+ */
24
+ chipBoundaryOverlap: number
18
25
  pathLength: number
19
26
  }
20
27
 
@@ -239,10 +239,6 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
239
239
  return penalty
240
240
  }
241
241
 
242
- private pathCost(path: Point[]): number {
243
- return this.pathLength(path) + this.getPinBandPenalty(path)
244
- }
245
-
246
242
  private isSegmentOutsidePinBand(a: Point, b: Point): boolean {
247
243
  if (isHorizontal(a, b)) {
248
244
  return a.y <= this.aabb.minY || a.y >= this.aabb.maxY
@@ -362,11 +358,15 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
362
358
  candidates.push(...mids)
363
359
  }
364
360
 
365
- // Generate new shifted paths, order by total path length (shorter first)
361
+ // Generate new shifted paths. Order by path length first, then prefer paths
362
+ // that stay out of the pin band. The pin-band preference is a tiebreaker
363
+ // only: it must never make the solver pick a longer route (a flat additive
364
+ // penalty would, discarding a shorter valid route in favor of a detour).
366
365
  const newStates: Array<{
367
366
  path: Point[]
368
367
  collisionRects: Set<ObstacleRect>
369
- len: number
368
+ length: number
369
+ pinBandPenalty: number
370
370
  }> = []
371
371
 
372
372
  const addShiftedCandidate = (
@@ -386,8 +386,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
386
386
  this.visited.add(key)
387
387
  const nextSet = new Set(collisionRects)
388
388
  nextSet.add(rect)
389
- const len = this.pathCost(newPath)
390
- newStates.push({ path: newPath, collisionRects: nextSet, len })
389
+ newStates.push({
390
+ path: newPath,
391
+ collisionRects: nextSet,
392
+ length: this.pathLength(newPath),
393
+ pinBandPenalty: this.getPinBandPenalty(newPath),
394
+ })
391
395
  }
392
396
 
393
397
  for (const coord of candidates) {
@@ -430,7 +434,9 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
430
434
  }
431
435
  }
432
436
 
433
- newStates.sort((a, b) => a.len - b.len)
437
+ newStates.sort(
438
+ (a, b) => a.length - b.length || a.pinBandPenalty - b.pinBandPenalty,
439
+ )
434
440
  for (const st of newStates) {
435
441
  this.queue.push({ path: st.path, collisionRects: st.collisionRects })
436
442
  }
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.92",
4
+ "version": "0.0.93",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",