@tscircuit/schematic-trace-solver 0.0.81 → 0.0.82

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
@@ -695,7 +695,7 @@ type CandidateLabel = {
695
695
  height: number;
696
696
  };
697
697
  type CandidateStatus$2 = "valid" | "chip-collision" | "text-collision" | "trace-collision" | "trace-clearance-violation" | "netlabel-collision";
698
- type CandidatePhase = "rotate" | "shift" | "lateral-shift";
698
+ type CandidatePhase = "rotate" | "trace-anchor" | "shift" | "lateral-shift";
699
699
  type EvaluatedCandidate = CandidateLabel & {
700
700
  status: CandidateStatus$2;
701
701
  selected: boolean;
@@ -733,6 +733,8 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
733
733
  private setCurrentLabel;
734
734
  private findCorrectedCandidate;
735
735
  private findValidRotatedCandidate;
736
+ private findValidTraceAnchorCandidate;
737
+ private getTraceAnchorCandidatePoints;
736
738
  private getAvailableOrientations;
737
739
  private findValidShiftedCandidate;
738
740
  /**
@@ -757,6 +759,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
757
759
  private getNetLabelWidth;
758
760
  private getNetLabelHeight;
759
761
  private getCandidateStatus;
762
+ private isAcceptableTraceAnchorChipCollision;
760
763
  private getBoundsStatus;
761
764
  private isTraceTooCloseToLabel;
762
765
  private getLabelTraceClearanceBounds;
package/dist/index.js CHANGED
@@ -6246,6 +6246,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
6246
6246
  this.addConnectorTrace(label, candidate, labelIndex);
6247
6247
  }
6248
6248
  addConnectorTrace(label, candidate, labelIndex) {
6249
+ if (candidate.phase === "trace-anchor") return;
6249
6250
  const tracePath = this.getCandidateConnectorTrace(label, candidate);
6250
6251
  if (tracePath.length < 2) return;
6251
6252
  const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`;
@@ -6280,6 +6281,12 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
6280
6281
  orientations
6281
6282
  );
6282
6283
  if (rotatedCandidate) return rotatedCandidate;
6284
+ const traceAnchorCandidate = this.findValidTraceAnchorCandidate(
6285
+ label,
6286
+ orientations[0],
6287
+ labelIndex
6288
+ );
6289
+ if (traceAnchorCandidate) return traceAnchorCandidate;
6283
6290
  const shiftedCandidate = this.findValidShiftedCandidate(
6284
6291
  label,
6285
6292
  orientations[0],
@@ -6313,6 +6320,46 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
6313
6320
  }
6314
6321
  return null;
6315
6322
  }
6323
+ findValidTraceAnchorCandidate(label, orientation, labelIndex) {
6324
+ const direction = dir(orientation);
6325
+ const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
6326
+ (a, b) => {
6327
+ const aAlongDirection = a.x * direction.x + a.y * direction.y;
6328
+ const bAlongDirection = b.x * direction.x + b.y * direction.y;
6329
+ return bAlongDirection - aAlongDirection;
6330
+ }
6331
+ );
6332
+ for (const anchorPoint of candidatePoints) {
6333
+ const candidate = this.createCandidate(label, anchorPoint, orientation);
6334
+ const result = this.evaluateCandidate(
6335
+ candidate,
6336
+ label,
6337
+ labelIndex,
6338
+ "trace-anchor"
6339
+ );
6340
+ this.currentCandidateResults.push(result);
6341
+ if (result.status === "valid") {
6342
+ result.selected = true;
6343
+ return result;
6344
+ }
6345
+ }
6346
+ return null;
6347
+ }
6348
+ getTraceAnchorCandidatePoints(label) {
6349
+ const seen = /* @__PURE__ */ new Set();
6350
+ const points = [];
6351
+ for (const traceId of label.mspConnectionPairIds ?? []) {
6352
+ const trace = this.traceMap[traceId];
6353
+ if (!trace) continue;
6354
+ for (const point of trace.tracePath) {
6355
+ const key = `${point.x.toFixed(9)},${point.y.toFixed(9)}`;
6356
+ if (seen.has(key)) continue;
6357
+ seen.add(key);
6358
+ points.push(point);
6359
+ }
6360
+ }
6361
+ return points;
6362
+ }
6316
6363
  getAvailableOrientations(label) {
6317
6364
  const effectiveNetId = label.netId ?? label.globalConnNetId;
6318
6365
  return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? [];
@@ -6581,16 +6628,28 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
6581
6628
  }
6582
6629
  getCandidateStatus(params) {
6583
6630
  const { candidate, label, labelIndex, phase } = params;
6631
+ const bounds = getRectBounds(
6632
+ candidate.center,
6633
+ candidate.width,
6634
+ candidate.height
6635
+ );
6584
6636
  const boundsStatus = this.getBoundsStatus({
6585
- bounds: getRectBounds(
6586
- candidate.center,
6587
- candidate.width,
6588
- candidate.height
6589
- ),
6637
+ bounds,
6590
6638
  labelIndex,
6591
6639
  label
6592
6640
  });
6593
- if (boundsStatus !== "valid") return boundsStatus;
6641
+ if (boundsStatus !== "valid") {
6642
+ if (phase !== "trace-anchor" || boundsStatus !== "chip-collision" || !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds)) {
6643
+ return boundsStatus;
6644
+ }
6645
+ const nonChipBoundsStatus = this.getBoundsStatus({
6646
+ bounds,
6647
+ labelIndex,
6648
+ label,
6649
+ ignoreChipCollisions: true
6650
+ });
6651
+ if (nonChipBoundsStatus !== "valid") return nonChipBoundsStatus;
6652
+ }
6594
6653
  const connectorTrace = this.getCandidateConnectorTrace(label, {
6595
6654
  anchorPoint: candidate.anchorPoint,
6596
6655
  orientation: candidate.orientation,
@@ -6613,17 +6672,35 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
6613
6672
  }
6614
6673
  return "valid";
6615
6674
  }
6675
+ isAcceptableTraceAnchorChipCollision(candidate, label, bounds) {
6676
+ const collidingChips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
6677
+ if (collidingChips.length === 0) return false;
6678
+ const labelChipIds = new Set(
6679
+ label.pinIds.map((pinId) => this.pinMap[pinId]?.chipId).filter((chipId) => Boolean(chipId))
6680
+ );
6681
+ return collidingChips.every((chip) => {
6682
+ if (!labelChipIds.has(chip.chipId)) return false;
6683
+ const { anchorPoint, orientation } = candidate;
6684
+ const chipBounds = chip.bounds;
6685
+ if (isYOrientation(orientation)) {
6686
+ return anchorPoint.x < chipBounds.minX - EPS8 || anchorPoint.x > chipBounds.maxX + EPS8;
6687
+ }
6688
+ return anchorPoint.y < chipBounds.minY - EPS8 || anchorPoint.y > chipBounds.maxY + EPS8;
6689
+ });
6690
+ }
6616
6691
  getBoundsStatus(candidateBoundsCheck) {
6617
- const { bounds, labelIndex, label } = candidateBoundsCheck;
6618
- if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
6619
- return "chip-collision";
6692
+ const { bounds, labelIndex, label, ignoreChipCollisions } = candidateBoundsCheck;
6693
+ if (!ignoreChipCollisions) {
6694
+ if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
6695
+ return "chip-collision";
6696
+ }
6697
+ if (this.sharesChipBoundary(bounds)) {
6698
+ return "chip-collision";
6699
+ }
6620
6700
  }
6621
6701
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
6622
6702
  return "text-collision";
6623
6703
  }
6624
- if (this.sharesChipBoundary(bounds)) {
6625
- return "chip-collision";
6626
- }
6627
6704
  if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
6628
6705
  return "trace-collision";
6629
6706
  }
@@ -146,6 +146,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
146
146
  candidate: EvaluatedCandidate,
147
147
  labelIndex: number,
148
148
  ) {
149
+ if (candidate.phase === "trace-anchor") return
150
+
149
151
  const tracePath = this.getCandidateConnectorTrace(label, candidate)
150
152
  if (tracePath.length < 2) return
151
153
 
@@ -187,6 +189,13 @@ export class AvailableNetOrientationSolver extends BaseSolver {
187
189
  )
188
190
  if (rotatedCandidate) return rotatedCandidate
189
191
 
192
+ const traceAnchorCandidate = this.findValidTraceAnchorCandidate(
193
+ label,
194
+ orientations[0]!,
195
+ labelIndex,
196
+ )
197
+ if (traceAnchorCandidate) return traceAnchorCandidate
198
+
190
199
  const shiftedCandidate = this.findValidShiftedCandidate(
191
200
  label,
192
201
  orientations[0]!,
@@ -228,6 +237,57 @@ export class AvailableNetOrientationSolver extends BaseSolver {
228
237
  return null
229
238
  }
230
239
 
240
+ private findValidTraceAnchorCandidate(
241
+ label: NetLabelPlacement,
242
+ orientation: FacingDirection,
243
+ labelIndex: number,
244
+ ) {
245
+ const direction = dir(orientation)
246
+ const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
247
+ (a, b) => {
248
+ const aAlongDirection = a.x * direction.x + a.y * direction.y
249
+ const bAlongDirection = b.x * direction.x + b.y * direction.y
250
+ return bAlongDirection - aAlongDirection
251
+ },
252
+ )
253
+
254
+ for (const anchorPoint of candidatePoints) {
255
+ const candidate = this.createCandidate(label, anchorPoint, orientation)
256
+ const result = this.evaluateCandidate(
257
+ candidate,
258
+ label,
259
+ labelIndex,
260
+ "trace-anchor",
261
+ )
262
+ this.currentCandidateResults.push(result)
263
+
264
+ if (result.status === "valid") {
265
+ result.selected = true
266
+ return result
267
+ }
268
+ }
269
+
270
+ return null
271
+ }
272
+
273
+ private getTraceAnchorCandidatePoints(label: NetLabelPlacement) {
274
+ const seen = new Set<string>()
275
+ const points: Point[] = []
276
+
277
+ for (const traceId of label.mspConnectionPairIds ?? []) {
278
+ const trace = this.traceMap[traceId]
279
+ if (!trace) continue
280
+ for (const point of trace.tracePath) {
281
+ const key = `${point.x.toFixed(9)},${point.y.toFixed(9)}`
282
+ if (seen.has(key)) continue
283
+ seen.add(key)
284
+ points.push(point)
285
+ }
286
+ }
287
+
288
+ return points
289
+ }
290
+
231
291
  private getAvailableOrientations(label: NetLabelPlacement) {
232
292
  const effectiveNetId = label.netId ?? label.globalConnNetId
233
293
  return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
@@ -608,16 +668,33 @@ export class AvailableNetOrientationSolver extends BaseSolver {
608
668
  phase: CandidatePhase
609
669
  }) {
610
670
  const { candidate, label, labelIndex, phase } = params
671
+ const bounds = getRectBounds(
672
+ candidate.center,
673
+ candidate.width,
674
+ candidate.height,
675
+ )
611
676
  const boundsStatus = this.getBoundsStatus({
612
- bounds: getRectBounds(
613
- candidate.center,
614
- candidate.width,
615
- candidate.height,
616
- ),
677
+ bounds,
617
678
  labelIndex,
618
679
  label,
619
680
  })
620
- if (boundsStatus !== "valid") return boundsStatus
681
+ if (boundsStatus !== "valid") {
682
+ if (
683
+ phase !== "trace-anchor" ||
684
+ boundsStatus !== "chip-collision" ||
685
+ !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds)
686
+ ) {
687
+ return boundsStatus
688
+ }
689
+
690
+ const nonChipBoundsStatus = this.getBoundsStatus({
691
+ bounds,
692
+ labelIndex,
693
+ label,
694
+ ignoreChipCollisions: true,
695
+ })
696
+ if (nonChipBoundsStatus !== "valid") return nonChipBoundsStatus
697
+ }
621
698
 
622
699
  const connectorTrace = this.getCandidateConnectorTrace(label, {
623
700
  anchorPoint: candidate.anchorPoint,
@@ -647,22 +724,59 @@ export class AvailableNetOrientationSolver extends BaseSolver {
647
724
  return "valid"
648
725
  }
649
726
 
727
+ private isAcceptableTraceAnchorChipCollision(
728
+ candidate: CandidateLabel,
729
+ label: NetLabelPlacement,
730
+ bounds: Bounds,
731
+ ) {
732
+ const collidingChips =
733
+ this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
734
+ if (collidingChips.length === 0) return false
735
+
736
+ const labelChipIds = new Set(
737
+ label.pinIds
738
+ .map((pinId) => this.pinMap[pinId]?.chipId)
739
+ .filter((chipId): chipId is string => Boolean(chipId)),
740
+ )
741
+
742
+ return collidingChips.every((chip) => {
743
+ if (!labelChipIds.has(chip.chipId)) return false
744
+
745
+ const { anchorPoint, orientation } = candidate
746
+ const chipBounds = chip.bounds
747
+ if (isYOrientation(orientation)) {
748
+ return (
749
+ anchorPoint.x < chipBounds.minX - EPS ||
750
+ anchorPoint.x > chipBounds.maxX + EPS
751
+ )
752
+ }
753
+ return (
754
+ anchorPoint.y < chipBounds.minY - EPS ||
755
+ anchorPoint.y > chipBounds.maxY + EPS
756
+ )
757
+ })
758
+ }
759
+
650
760
  private getBoundsStatus(candidateBoundsCheck: {
651
761
  bounds: Bounds
652
762
  labelIndex: number
653
763
  label: NetLabelPlacement
764
+ ignoreChipCollisions?: boolean
654
765
  }): CandidateStatus {
655
- const { bounds, labelIndex, label } = candidateBoundsCheck
766
+ const { bounds, labelIndex, label, ignoreChipCollisions } =
767
+ candidateBoundsCheck
656
768
 
657
- if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
658
- return "chip-collision"
769
+ if (!ignoreChipCollisions) {
770
+ if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
771
+ return "chip-collision"
772
+ }
773
+ if (this.sharesChipBoundary(bounds)) {
774
+ return "chip-collision"
775
+ }
659
776
  }
660
777
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
661
778
  return "text-collision"
662
779
  }
663
- if (this.sharesChipBoundary(bounds)) {
664
- return "chip-collision"
665
- }
666
780
  if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
667
781
  return "trace-collision"
668
782
  }
@@ -35,7 +35,11 @@ export type CandidateStatus =
35
35
  | "trace-clearance-violation"
36
36
  | "netlabel-collision"
37
37
 
38
- export type CandidatePhase = "rotate" | "shift" | "lateral-shift"
38
+ export type CandidatePhase =
39
+ | "rotate"
40
+ | "trace-anchor"
41
+ | "shift"
42
+ | "lateral-shift"
39
43
 
40
44
  export type EvaluatedCandidate = CandidateLabel & {
41
45
  status: CandidateStatus
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.81",
4
+ "version": "0.0.82",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,68 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+
5
+ const inputProblem: InputProblem = {
6
+ chips: [
7
+ {
8
+ chipId: "D1",
9
+ center: { x: 7.9, y: 0 },
10
+ width: 0.7400000000000002,
11
+ height: 1.04,
12
+ pins: [
13
+ { pinId: "D1.1", x: 8, y: -0.52 },
14
+ { pinId: "D1.2", x: 8, y: 0.52 },
15
+ ],
16
+ },
17
+ {
18
+ chipId: "D2",
19
+ center: { x: 10, y: 0.07 },
20
+ width: 1.0399999999999991,
21
+ height: 0.68,
22
+ pins: [
23
+ { pinId: "D2.1", x: 9.48, y: 0 },
24
+ { pinId: "D2.2", x: 10.52, y: 0 },
25
+ ],
26
+ },
27
+ ],
28
+ directConnections: [],
29
+ netConnections: [
30
+ {
31
+ netId: "VCC",
32
+ pinIds: ["D1.1", "D2.1"],
33
+ netLabelWidth: 0.48,
34
+ netLabelHeight: 0.42,
35
+ },
36
+ {
37
+ netId: "GND",
38
+ pinIds: ["D1.2", "D2.2"],
39
+ netLabelWidth: 0.48,
40
+ netLabelHeight: 0.42,
41
+ },
42
+ ],
43
+ textBoxes: [],
44
+ availableNetLabelOrientations: {
45
+ GND: ["y-"],
46
+ VCC: ["y+"],
47
+ },
48
+ maxMspPairDistance: 0.1,
49
+ }
50
+
51
+ test("rotated component rail label stays on the existing trace", () => {
52
+ const solver = new SchematicTracePipelineSolver(inputProblem)
53
+ solver.solve()
54
+
55
+ const traces = solver.netLabelTraceCollisionSolver!.getOutput().traces
56
+ const labels =
57
+ solver.netLabelNetLabelCollisionSolver!.getOutput().netLabelPlacements
58
+ const gndLabel = labels.find((label) => label.netId === "GND")
59
+
60
+ expect(
61
+ traces.some((trace) =>
62
+ trace.mspPairId.startsWith("available-net-orientation"),
63
+ ),
64
+ ).toBe(false)
65
+ expect(gndLabel?.orientation).toBe("y-")
66
+ expect(gndLabel?.anchorPoint.x).toBeCloseTo(10.72)
67
+ expect(gndLabel?.anchorPoint.y).toBeCloseTo(0)
68
+ })