@tscircuit/schematic-trace-solver 0.0.109 → 0.0.111

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.js CHANGED
@@ -3259,11 +3259,18 @@ var groupLabelsByChipAndOrientation = ({
3259
3259
  chips
3260
3260
  }) => {
3261
3261
  const groupedLabels = {};
3262
+ const chipIdByPinId = new Map(
3263
+ chips.flatMap(
3264
+ (chip) => chip.pins.map((pin) => [pin.pinId, chip.chipId])
3265
+ )
3266
+ );
3262
3267
  for (const label of labels) {
3263
3268
  if (label.pinIds.length === 0) {
3264
3269
  continue;
3265
3270
  }
3266
- const chipId = label.pinIds[0].split(".")[0];
3271
+ const pinId = label.pinIds[0];
3272
+ const legacyChipId = pinId.includes(".") ? pinId.split(".")[0] : void 0;
3273
+ const chipId = legacyChipId ?? chipIdByPinId.get(pinId);
3267
3274
  if (!chipId) {
3268
3275
  continue;
3269
3276
  }
@@ -8147,17 +8154,44 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8147
8154
  return Math.min(this.maxSearchDistance, labelLength * 2);
8148
8155
  }
8149
8156
  getLateralColumnMaxDistance(label, orientation, baseAnchor) {
8150
- const chipId = label.pinIds.map((pid) => this.pinMap[pid]?.chipId).find(Boolean);
8151
- const chip = chipId ? this.chipObstacleSpatialIndex.chips.find((c) => c.chipId === chipId) : null;
8152
- if (chip) {
8153
- if (orientation === "y-")
8154
- return Math.max(0, baseAnchor.y - chip.bounds.minY);
8155
- if (orientation === "y+")
8156
- return Math.max(0, chip.bounds.maxY - baseAnchor.y);
8157
- if (orientation === "x-")
8158
- return Math.max(0, baseAnchor.x - chip.bounds.minX);
8159
- if (orientation === "x+")
8160
- return Math.max(0, chip.bounds.maxX - baseAnchor.x);
8157
+ const availableOrientations = this.getAvailableOrientations(label);
8158
+ const isSingleVerticalOrientation = availableOrientations.length === 1 && isYOrientation(availableOrientations[0]);
8159
+ const labelChipIds = new Set(
8160
+ label.pinIds.map((pinId) => this.pinMap[pinId]?.chipId).filter((chipId) => Boolean(chipId))
8161
+ );
8162
+ const connectedChips = this.chipObstacleSpatialIndex.chips.filter(
8163
+ (chip) => labelChipIds.has(chip.chipId)
8164
+ );
8165
+ const firstConnectedChipId = label.pinIds.map((pinId) => this.pinMap[pinId]?.chipId).find(Boolean);
8166
+ const firstConnectedChip = connectedChips.find(
8167
+ (chip) => chip.chipId === firstConnectedChipId
8168
+ );
8169
+ const labelChips = isSingleVerticalOrientation ? connectedChips : firstConnectedChip ? [firstConnectedChip] : [];
8170
+ if (labelChips.length > 0) {
8171
+ if (orientation === "y-") {
8172
+ return Math.max(
8173
+ 0,
8174
+ ...labelChips.map((chip) => baseAnchor.y - chip.bounds.minY)
8175
+ );
8176
+ }
8177
+ if (orientation === "y+") {
8178
+ return Math.max(
8179
+ 0,
8180
+ ...labelChips.map((chip) => chip.bounds.maxY - baseAnchor.y)
8181
+ );
8182
+ }
8183
+ if (orientation === "x-") {
8184
+ return Math.max(
8185
+ 0,
8186
+ ...labelChips.map((chip) => baseAnchor.x - chip.bounds.minX)
8187
+ );
8188
+ }
8189
+ if (orientation === "x+") {
8190
+ return Math.max(
8191
+ 0,
8192
+ ...labelChips.map((chip) => chip.bounds.maxX - baseAnchor.x)
8193
+ );
8194
+ }
8161
8195
  }
8162
8196
  return this.getSearchDistanceLimit(label, orientation);
8163
8197
  }
@@ -640,22 +640,61 @@ export class AvailableNetOrientationSolver extends BaseSolver {
640
640
  orientation: FacingDirection,
641
641
  baseAnchor: Point,
642
642
  ) {
643
- const chipId = label.pinIds
644
- .map((pid) => this.pinMap[pid]?.chipId)
643
+ const availableOrientations = this.getAvailableOrientations(label)
644
+ const isSingleVerticalOrientation =
645
+ availableOrientations.length === 1 &&
646
+ isYOrientation(availableOrientations[0]!)
647
+
648
+ // A vertical rail between components may need to branch laterally and then
649
+ // search past either connected component before it can use its required
650
+ // orientation. Keep other labels scoped to their first connected component
651
+ // so an orientation correction does not move a signal label across the
652
+ // entire connection.
653
+ const labelChipIds = new Set(
654
+ label.pinIds
655
+ .map((pinId) => this.pinMap[pinId]?.chipId)
656
+ .filter((chipId): chipId is string => Boolean(chipId)),
657
+ )
658
+ const connectedChips = this.chipObstacleSpatialIndex.chips.filter((chip) =>
659
+ labelChipIds.has(chip.chipId),
660
+ )
661
+ const firstConnectedChipId = label.pinIds
662
+ .map((pinId) => this.pinMap[pinId]?.chipId)
645
663
  .find(Boolean)
646
- const chip = chipId
647
- ? this.chipObstacleSpatialIndex.chips.find((c) => c.chipId === chipId)
648
- : null
649
-
650
- if (chip) {
651
- if (orientation === "y-")
652
- return Math.max(0, baseAnchor.y - chip.bounds.minY)
653
- if (orientation === "y+")
654
- return Math.max(0, chip.bounds.maxY - baseAnchor.y)
655
- if (orientation === "x-")
656
- return Math.max(0, baseAnchor.x - chip.bounds.minX)
657
- if (orientation === "x+")
658
- return Math.max(0, chip.bounds.maxX - baseAnchor.x)
664
+ const firstConnectedChip = connectedChips.find(
665
+ (chip) => chip.chipId === firstConnectedChipId,
666
+ )
667
+ const labelChips = isSingleVerticalOrientation
668
+ ? connectedChips
669
+ : firstConnectedChip
670
+ ? [firstConnectedChip]
671
+ : []
672
+
673
+ if (labelChips.length > 0) {
674
+ if (orientation === "y-") {
675
+ return Math.max(
676
+ 0,
677
+ ...labelChips.map((chip) => baseAnchor.y - chip.bounds.minY),
678
+ )
679
+ }
680
+ if (orientation === "y+") {
681
+ return Math.max(
682
+ 0,
683
+ ...labelChips.map((chip) => chip.bounds.maxY - baseAnchor.y),
684
+ )
685
+ }
686
+ if (orientation === "x-") {
687
+ return Math.max(
688
+ 0,
689
+ ...labelChips.map((chip) => baseAnchor.x - chip.bounds.minX),
690
+ )
691
+ }
692
+ if (orientation === "x+") {
693
+ return Math.max(
694
+ 0,
695
+ ...labelChips.map((chip) => chip.bounds.maxX - baseAnchor.x),
696
+ )
697
+ }
659
698
  }
660
699
 
661
700
  return this.getSearchDistanceLimit(label, orientation)
@@ -7,7 +7,8 @@ import type { InputProblem } from "lib/types/InputProblem"
7
7
  * with the same orientation relative to that chip.
8
8
  *
9
9
  * @param labels An array of NetLabelPlacement objects to be grouped.
10
- * @param chips An array of Chip objects from the InputProblem. (Currently not directly used for grouping logic, but part of the signature).
10
+ * @param chips Chips from the input problem, used to resolve opaque pin IDs to
11
+ * their owning component.
11
12
  * @returns A record where keys are in the format "chipId-orientation" (e.g., "U1-left")
12
13
  * and values are arrays of NetLabelPlacement objects belonging to that group.
13
14
  */
@@ -19,6 +20,11 @@ export const groupLabelsByChipAndOrientation = ({
19
20
  chips: InputProblem["chips"]
20
21
  }): Record<string, NetLabelPlacement[]> => {
21
22
  const groupedLabels: Record<string, NetLabelPlacement[]> = {}
23
+ const chipIdByPinId = new Map(
24
+ chips.flatMap((chip) =>
25
+ chip.pins.map((pin) => [pin.pinId, chip.chipId] as const),
26
+ ),
27
+ )
22
28
 
23
29
  for (const label of labels) {
24
30
  if (label.pinIds.length === 0) {
@@ -26,10 +32,12 @@ export const groupLabelsByChipAndOrientation = ({
26
32
  continue
27
33
  }
28
34
 
29
- // Extract chipId from the first pinId (e.g., "U1.1" -> "U1")
30
- const chipId = label.pinIds[0].split(".")[0]
35
+ const pinId = label.pinIds[0]!
36
+ // Preserve historical group IDs for component-qualified pins while using
37
+ // the input's pin ownership for opaque IDs such as schematic_port_123.
38
+ const legacyChipId = pinId.includes(".") ? pinId.split(".")[0] : undefined
39
+ const chipId = legacyChipId ?? chipIdByPinId.get(pinId)
31
40
  if (!chipId) {
32
- // Should not happen if pinIds are well-formed, but good to guard
33
41
  continue
34
42
  }
35
43
 
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.109",
4
+ "version": "0.0.111",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,48 @@
1
+ <svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="0.2,0.3 -0.2,-0.3999999999999999" data-type="line" data-label="" points="315.05882352941177,281.5686274509804 271.13725490196083,358.4313725490196" fill="none" stroke="hsl(157, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="0.2,0.30000000000000004 0.2,-0.04999999999999993 -0.2,-0.04999999999999993 -0.2,-0.3999999999999999" data-type="line" data-label="" points="315.05882352941177,281.5686274509804 315.05882352941177,320 271.13725490196083,320 271.13725490196083,358.4313725490196" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="0.2,-0.04999999999999993 0.2,6.938893903907228e-17 0.65,6.938893903907228e-17 0.65,-0.10099999999999994" data-type="line" data-label="" points="315.05882352941177,320 315.05882352941177,314.5098039215686 364.47058823529414,314.5098039215686 364.47058823529414,325.59999999999997" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="TOP" data-x="0" data-y="1.4" x="249.1764705882353" y="40" width="87.84313725490199" height="241.5686274509804" fill="hsl(93, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009107142857142857"/></g><g><rect data-type="rect" data-label="BOTTOM" data-x="0" data-y="-1.5" x="249.1764705882353" y="358.43137254901967" width="87.84313725490199" height="241.5686274509804" fill="hsl(259, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009107142857142857"/></g><g><rect data-type="rect" data-label="netId: GND
2
+ globalConnNetId: connectivity_net0" data-x="0.65" data-y="-0.31099999999999994" x="338.11764705882354" y="325.59999999999997" width="52.70588235294122" height="46.117647058823536" fill="#00000066" stroke="#000000" stroke-width="0.009107142857142857"/></g><g><circle data-type="point" data-label="TOP.GND
3
+ y-" data-x="0.2" data-y="0.3" cx="315.05882352941177" cy="281.5686274509804" r="3" fill="hsl(116, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="BOTTOM.GND
4
+ y+" data-x="-0.2" data-y="-0.3999999999999999" cx="271.13725490196083" cy="358.4313725490196" r="3" fill="hsl(42, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="anchorPoint
5
+ orientation: y-" data-x="0.65" data-y="-0.10099999999999994" cx="364.47058823529414" cy="325.59999999999997" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
6
+ document.currentScript.parentElement.addEventListener('mousemove', (e) => {
7
+ const svg = e.currentTarget;
8
+ const rect = svg.getBoundingClientRect();
9
+ const x = e.clientX - rect.left;
10
+ const y = e.clientY - rect.top;
11
+ const crosshair = svg.getElementById('crosshair');
12
+ const h = svg.getElementById('crosshair-h');
13
+ const v = svg.getElementById('crosshair-v');
14
+ const coords = svg.getElementById('coordinates');
15
+
16
+ crosshair.style.display = 'block';
17
+ h.setAttribute('x1', '0');
18
+ h.setAttribute('x2', '640');
19
+ h.setAttribute('y1', y);
20
+ h.setAttribute('y2', y);
21
+ v.setAttribute('x1', x);
22
+ v.setAttribute('x2', x);
23
+ v.setAttribute('y1', '0');
24
+ v.setAttribute('y2', '640');
25
+
26
+ // Calculate real coordinates using inverse transformation
27
+ const matrix = {"a":109.80392156862746,"c":0,"e":293.0980392156863,"b":0,"d":-109.80392156862746,"f":314.5098039215686};
28
+ // Manually invert and apply the affine transform
29
+ // Since we only use translate and scale, we can directly compute:
30
+ // x' = (x - tx) / sx
31
+ // y' = (y - ty) / sy
32
+ const sx = matrix.a;
33
+ const sy = matrix.d;
34
+ const tx = matrix.e;
35
+ const ty = matrix.f;
36
+ const realPoint = {
37
+ x: (x - tx) / sx,
38
+ y: (y - ty) / sy // Flip y back since we used negative scale
39
+ }
40
+
41
+ coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
42
+ coords.setAttribute('x', (x + 5).toString());
43
+ coords.setAttribute('y', (y - 5).toString());
44
+ });
45
+ document.currentScript.parentElement.addEventListener('mouseleave', () => {
46
+ document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
47
+ });
48
+ ]]></script></svg>
@@ -0,0 +1,69 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import "tests/fixtures/matcher"
5
+
6
+ const inputProblem: InputProblem = {
7
+ chips: [
8
+ {
9
+ chipId: "TOP",
10
+ center: { x: 0, y: 1.4 },
11
+ width: 0.8,
12
+ height: 1.4,
13
+ pins: [
14
+ {
15
+ pinId: "TOP.GND",
16
+ x: 0.2,
17
+ y: 0.3,
18
+ _facingDirection: "y-",
19
+ },
20
+ ],
21
+ },
22
+ {
23
+ chipId: "BOTTOM",
24
+ center: { x: 0, y: -1.5 },
25
+ width: 0.8,
26
+ height: 1.4,
27
+ pins: [
28
+ {
29
+ pinId: "BOTTOM.GND",
30
+ x: -0.2,
31
+ y: -0.4,
32
+ _facingDirection: "y+",
33
+ },
34
+ ],
35
+ },
36
+ ],
37
+ directConnections: [],
38
+ netConnections: [
39
+ {
40
+ netId: "GND",
41
+ pinIds: ["TOP.GND", "BOTTOM.GND"],
42
+ netLabelWidth: 0.42,
43
+ netLabelHeight: 0.48,
44
+ },
45
+ ],
46
+ textBoxes: [],
47
+ availableNetLabelOrientations: { GND: ["y-"] },
48
+ maxMspPairDistance: 2.4,
49
+ }
50
+
51
+ test("repro84 restores GND orientation between stacked components", () => {
52
+ const solver = new SchematicTracePipelineSolver(inputProblem)
53
+
54
+ solver.solve()
55
+
56
+ const initialGndLabel = solver.netLabelPlacementSolver!.netLabelPlacements[0]
57
+ const finalGndLabel =
58
+ solver.netLabelNetLabelCollisionSolver!.getOutput().netLabelPlacements[0]
59
+ const traces = solver.netLabelTraceCollisionSolver!.getOutput().traces
60
+
61
+ expect(initialGndLabel?.orientation).toBe("x+")
62
+ expect(finalGndLabel?.orientation).toBe("y-")
63
+ expect(
64
+ traces.some((trace) =>
65
+ trace.mspPairId.startsWith("available-net-orientation"),
66
+ ),
67
+ ).toBe(true)
68
+ expect(solver).toMatchSolverSnapshot(import.meta.path)
69
+ })