@tscircuit/schematic-trace-solver 0.0.4 → 0.0.6

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.
@@ -2,5 +2,6 @@
2
2
  "$schema": "http://json.schemastore.org/cosmos-config",
3
3
  "plugins": ["react-cosmos-plugin-vite"],
4
4
  "fixtureFileSuffix": "page",
5
- "decoratorFile": "cosmos.decorator.tsx"
5
+ "decoratorFile": "cosmos.decorator.tsx",
6
+ "port": 5020
6
7
  }
package/dist/index.d.ts CHANGED
@@ -85,6 +85,7 @@ interface InputProblem {
85
85
  directConnections: Array<InputDirectConnection>;
86
86
  netConnections: Array<InputNetConnection>;
87
87
  availableNetLabelOrientations: Record<NetId, FacingDirection[]>;
88
+ maxMspPairDistance?: number;
88
89
  _chipObstacleSpatialIndex?: ChipObstacleSpatialIndex;
89
90
  }
90
91
 
@@ -107,6 +108,7 @@ declare class MspConnectionPairSolver extends BaseSolver {
107
108
  globalConnMap: ConnectivityMap;
108
109
  queuedDcNetIds: string[];
109
110
  chipMap: Record<string, InputChip>;
111
+ maxMspPairDistance: number;
110
112
  pinMap: Record<string, InputPin & {
111
113
  chipId: string;
112
114
  }>;
@@ -339,7 +341,8 @@ declare class SingleNetLabelPlacementSolver extends BaseSolver {
339
341
  type OverlappingSameNetTraceGroup = {
340
342
  globalConnNetId: string;
341
343
  netId?: string;
342
- overlappingTraces: SolvedTracePath;
344
+ overlappingTraces?: SolvedTracePath;
345
+ portOnlyPinId?: string;
343
346
  };
344
347
  interface NetLabelPlacement {
345
348
  globalConnNetId: string;
@@ -406,6 +409,7 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
406
409
  pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof GuidelinesSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver>)[];
407
410
  constructor(inputProblem: InputProblem);
408
411
  currentPipelineStepIndex: number;
412
+ private cloneAndCorrectInputProblem;
409
413
  _step(): void;
410
414
  solveUntilPhase(phase: string): void;
411
415
  getCurrentPhase(): string;
package/dist/index.js CHANGED
@@ -5274,8 +5274,9 @@ var getConnectivityMapsFromInputProblem = (inputProblem) => {
5274
5274
  };
5275
5275
 
5276
5276
  // lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts
5277
- function getOrthogonalMinimumSpanningTree(pins) {
5277
+ function getOrthogonalMinimumSpanningTree(pins, opts = {}) {
5278
5278
  const n = pins.length;
5279
+ const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY;
5279
5280
  if (n <= 1) return [];
5280
5281
  {
5281
5282
  const seen = /* @__PURE__ */ new Set();
@@ -5316,7 +5317,8 @@ function getOrthogonalMinimumSpanningTree(pins) {
5316
5317
  }
5317
5318
  for (let v = 0; v < n; v++) {
5318
5319
  if (!inTree[v]) {
5319
- const d = manhattan(pins[u], pins[v]);
5320
+ const d0 = manhattan(pins[u], pins[v]);
5321
+ const d = d0 > maxDistance ? Number.POSITIVE_INFINITY : d0;
5320
5322
  if (d < bestDist[v] || d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId) {
5321
5323
  bestDist[v] = d;
5322
5324
  parent[v] = u;
@@ -5446,11 +5448,13 @@ var MspConnectionPairSolver = class extends BaseSolver {
5446
5448
  globalConnMap;
5447
5449
  queuedDcNetIds;
5448
5450
  chipMap;
5451
+ maxMspPairDistance;
5449
5452
  pinMap;
5450
5453
  userNetIdByPinId;
5451
5454
  constructor({ inputProblem }) {
5452
5455
  super();
5453
5456
  this.inputProblem = inputProblem;
5457
+ this.maxMspPairDistance = inputProblem.maxMspPairDistance ?? 1;
5454
5458
  const { directConnMap, netConnMap } = getConnectivityMapsFromInputProblem(inputProblem);
5455
5459
  this.dcConnMap = directConnMap;
5456
5460
  this.globalConnMap = netConnMap;
@@ -5508,7 +5512,8 @@ var MspConnectionPairSolver = class extends BaseSolver {
5508
5512
  return;
5509
5513
  }
5510
5514
  const msp = getOrthogonalMinimumSpanningTree(
5511
- directlyConnectedPins.map((p) => this.pinMap[p])
5515
+ directlyConnectedPins.map((p) => this.pinMap[p]),
5516
+ { maxDistance: this.maxMspPairDistance }
5512
5517
  );
5513
5518
  for (const [pin1, pin2] of msp) {
5514
5519
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1);
@@ -7232,6 +7237,92 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7232
7237
  this.solved = true;
7233
7238
  return;
7234
7239
  }
7240
+ if (this.overlappingSameNetTraceGroup.portOnlyPinId) {
7241
+ const pinId = this.overlappingSameNetTraceGroup.portOnlyPinId;
7242
+ let pin = null;
7243
+ for (const chip of this.inputProblem.chips) {
7244
+ const p = chip.pins.find((pp) => pp.pinId === pinId);
7245
+ if (p) {
7246
+ pin = { x: p.x, y: p.y };
7247
+ break;
7248
+ }
7249
+ }
7250
+ if (!pin) {
7251
+ this.failed = true;
7252
+ this.error = `Port-only pin not found: ${pinId}`;
7253
+ return;
7254
+ }
7255
+ const orientations2 = this.availableOrientations.length > 0 ? this.availableOrientations : ["x+", "x-", "y+", "y-"];
7256
+ const anchor = { x: pin.x, y: pin.y };
7257
+ const outwardOf = (o) => o === "x+" ? { x: 1, y: 0 } : o === "x-" ? { x: -1, y: 0 } : o === "y+" ? { x: 0, y: 1 } : { x: 0, y: -1 };
7258
+ for (const orientation of orientations2) {
7259
+ const { width, height } = this.getDimsForOrientation(orientation);
7260
+ const baseCenter = this.getCenterFromAnchor(
7261
+ anchor,
7262
+ orientation,
7263
+ width,
7264
+ height
7265
+ );
7266
+ const outward = outwardOf(orientation);
7267
+ const offset = 1e-3;
7268
+ const center = {
7269
+ x: baseCenter.x + outward.x * offset,
7270
+ y: baseCenter.y + outward.y * offset
7271
+ };
7272
+ const bounds = this.getRectBounds(center, width, height);
7273
+ const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
7274
+ if (chips.length > 0) {
7275
+ this.testedCandidates.push({
7276
+ center,
7277
+ width,
7278
+ height,
7279
+ bounds,
7280
+ anchor,
7281
+ orientation,
7282
+ status: "chip-collision",
7283
+ hostSegIndex: -1
7284
+ });
7285
+ continue;
7286
+ }
7287
+ if (this.rectIntersectsAnyTrace(bounds, "", -1)) {
7288
+ this.testedCandidates.push({
7289
+ center,
7290
+ width,
7291
+ height,
7292
+ bounds,
7293
+ anchor,
7294
+ orientation,
7295
+ status: "trace-collision",
7296
+ hostSegIndex: -1
7297
+ });
7298
+ continue;
7299
+ }
7300
+ this.testedCandidates.push({
7301
+ center,
7302
+ width,
7303
+ height,
7304
+ bounds,
7305
+ anchor,
7306
+ orientation,
7307
+ status: "ok",
7308
+ hostSegIndex: -1
7309
+ });
7310
+ this.netLabelPlacement = {
7311
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
7312
+ dcConnNetId: void 0,
7313
+ orientation,
7314
+ anchorPoint: anchor,
7315
+ width,
7316
+ height,
7317
+ center
7318
+ };
7319
+ this.solved = true;
7320
+ return;
7321
+ }
7322
+ this.failed = true;
7323
+ this.error = "Could not place net label at port without collisions";
7324
+ return;
7325
+ }
7235
7326
  const groupId = this.overlappingSameNetTraceGroup.globalConnNetId;
7236
7327
  const chipsById = Object.fromEntries(
7237
7328
  this.inputProblem.chips.map((c) => [c.chipId, c])
@@ -7266,8 +7357,13 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7266
7357
  (t) => t.pins[0].chipId === largestChipId || t.pins[1].chipId === largestChipId
7267
7358
  );
7268
7359
  let host = hostCandidates.length > 0 ? hostCandidates.reduce((a, b) => lengthOf(a) >= lengthOf(b) ? a : b) : this.overlappingSameNetTraceGroup.overlappingTraces;
7360
+ if (!host) {
7361
+ this.failed = true;
7362
+ this.error = "No host trace found for net label placement";
7363
+ return;
7364
+ }
7269
7365
  let pts = host.tracePath.slice();
7270
- if (largestChipId) {
7366
+ if (largestChipId && host) {
7271
7367
  const largePin = host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1];
7272
7368
  const d0 = Math.abs(pts[0].x - largePin.x) + Math.abs(pts[0].y - largePin.y);
7273
7369
  const dL = Math.abs(pts[pts.length - 1].x - largePin.x) + Math.abs(pts[pts.length - 1].y - largePin.y);
@@ -7406,7 +7502,7 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7406
7502
  const groupFill = getColorFromString(groupId, 0.5);
7407
7503
  for (const trace of Object.values(this.inputTraceMap)) {
7408
7504
  if (trace.globalConnNetId !== groupId) continue;
7409
- const isHost = trace.mspPairId === host.mspPairId;
7505
+ const isHost = host ? trace.mspPairId === host.mspPairId : false;
7410
7506
  graphics.lines.push({
7411
7507
  points: trace.tracePath,
7412
7508
  strokeColor: isHost ? groupStroke : groupFill,
@@ -7475,32 +7571,100 @@ var NetLabelPlacementSolver = class extends BaseSolver {
7475
7571
  if (!byGlobal[key]) byGlobal[key] = [];
7476
7572
  byGlobal[key].push(trace);
7477
7573
  }
7574
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
7575
+ this.inputProblem
7576
+ );
7577
+ const userNetIdByPinId = {};
7578
+ for (const dc of this.inputProblem.directConnections) {
7579
+ if (dc.netId) {
7580
+ const [a, b] = dc.pinIds;
7581
+ userNetIdByPinId[a] = dc.netId;
7582
+ userNetIdByPinId[b] = dc.netId;
7583
+ }
7584
+ }
7585
+ for (const nc of this.inputProblem.netConnections) {
7586
+ for (const pid of nc.pinIds) {
7587
+ userNetIdByPinId[pid] = nc.netId;
7588
+ }
7589
+ }
7478
7590
  const groups = [];
7479
- for (const [globalConnNetId, traces] of Object.entries(byGlobal)) {
7480
- if (traces.length === 0) continue;
7481
- const lengthOf = (path) => {
7482
- let sum = 0;
7483
- const pts = path.tracePath;
7484
- for (let i = 0; i < pts.length - 1; i++) {
7485
- sum += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y);
7486
- }
7487
- return sum;
7488
- };
7489
- let rep = traces[0];
7490
- let repLen = lengthOf(rep);
7491
- for (let i = 1; i < traces.length; i++) {
7492
- const len = lengthOf(traces[i]);
7493
- if (len > repLen) {
7494
- rep = traces[i];
7495
- repLen = len;
7591
+ for (const globalConnNetId of Object.keys(netConnMap.netMap)) {
7592
+ const pinsInNet = netConnMap.getIdsConnectedToNet(
7593
+ globalConnNetId
7594
+ );
7595
+ const adj = {};
7596
+ for (const pid of pinsInNet) adj[pid] = /* @__PURE__ */ new Set();
7597
+ for (const t of byGlobal[globalConnNetId] ?? []) {
7598
+ const a = t.pins[0].pinId;
7599
+ const b = t.pins[1].pinId;
7600
+ if (adj[a] && adj[b]) {
7601
+ adj[a].add(b);
7602
+ adj[b].add(a);
7603
+ }
7604
+ }
7605
+ const visited = /* @__PURE__ */ new Set();
7606
+ for (const pid of pinsInNet) {
7607
+ if (visited.has(pid)) continue;
7608
+ const stack = [pid];
7609
+ const component = /* @__PURE__ */ new Set();
7610
+ visited.add(pid);
7611
+ while (stack.length > 0) {
7612
+ const u = stack.pop();
7613
+ component.add(u);
7614
+ for (const v of adj[u] ?? []) {
7615
+ if (!visited.has(v)) {
7616
+ visited.add(v);
7617
+ stack.push(v);
7618
+ }
7619
+ }
7620
+ }
7621
+ const compTraces = (byGlobal[globalConnNetId] ?? []).filter(
7622
+ (t) => component.has(t.pins[0].pinId) && component.has(t.pins[1].pinId)
7623
+ );
7624
+ if (compTraces.length > 0) {
7625
+ const lengthOf = (path) => {
7626
+ let sum = 0;
7627
+ const pts = path.tracePath;
7628
+ for (let i = 0; i < pts.length - 1; i++) {
7629
+ sum += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y);
7630
+ }
7631
+ return sum;
7632
+ };
7633
+ let rep = compTraces[0];
7634
+ let repLen = lengthOf(rep);
7635
+ for (let i = 1; i < compTraces.length; i++) {
7636
+ const len = lengthOf(compTraces[i]);
7637
+ if (len > repLen) {
7638
+ rep = compTraces[i];
7639
+ repLen = len;
7640
+ }
7641
+ }
7642
+ let userNetId = compTraces.find((t) => t.userNetId != null)?.userNetId;
7643
+ if (!userNetId) {
7644
+ for (const p of component) {
7645
+ if (userNetIdByPinId[p]) {
7646
+ userNetId = userNetIdByPinId[p];
7647
+ break;
7648
+ }
7649
+ }
7650
+ }
7651
+ groups.push({
7652
+ globalConnNetId,
7653
+ netId: userNetId,
7654
+ overlappingTraces: rep
7655
+ });
7656
+ } else {
7657
+ for (const p of component) {
7658
+ const userNetId = userNetIdByPinId[p];
7659
+ if (!userNetId) continue;
7660
+ groups.push({
7661
+ globalConnNetId,
7662
+ netId: userNetId,
7663
+ portOnlyPinId: p
7664
+ });
7665
+ }
7496
7666
  }
7497
7667
  }
7498
- const userNetId = traces.find((t) => t.userNetId != null)?.userNetId;
7499
- groups.push({
7500
- globalConnNetId,
7501
- netId: userNetId,
7502
- overlappingTraces: rep
7503
- });
7504
7668
  }
7505
7669
  return groups;
7506
7670
  }
@@ -7701,6 +7865,32 @@ var GuidelinesSolver = class extends BaseSolver {
7701
7865
  }
7702
7866
  };
7703
7867
 
7868
+ // lib/solvers/SchematicTracePipelineSolver/correctPinsInsideChip.ts
7869
+ var correctPinsInsideChips = (problem) => {
7870
+ for (const chip of problem.chips) {
7871
+ const bounds = getInputChipBounds(chip);
7872
+ for (const pin of chip.pins) {
7873
+ const isInside = pin.x > bounds.minX && pin.x < bounds.maxX && pin.y > bounds.minY && pin.y < bounds.maxY;
7874
+ if (!isInside) continue;
7875
+ const distLeft = pin.x - bounds.minX;
7876
+ const distRight = bounds.maxX - pin.x;
7877
+ const distBottom = pin.y - bounds.minY;
7878
+ const distTop = bounds.maxY - pin.y;
7879
+ const minDist = Math.min(distLeft, distRight, distBottom, distTop);
7880
+ if (minDist === distLeft) {
7881
+ pin.x = bounds.minX;
7882
+ } else if (minDist === distRight) {
7883
+ pin.x = bounds.maxX;
7884
+ } else if (minDist === distBottom) {
7885
+ pin.y = bounds.minY;
7886
+ } else {
7887
+ pin.y = bounds.maxY;
7888
+ }
7889
+ pin._facingDirection = void 0;
7890
+ }
7891
+ }
7892
+ };
7893
+
7704
7894
  // lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts
7705
7895
  function definePipelineStep(solverName, solverClass, getConstructorParams, opts = {}) {
7706
7896
  return {
@@ -7799,7 +7989,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
7799
7989
  ];
7800
7990
  constructor(inputProblem) {
7801
7991
  super();
7802
- this.inputProblem = inputProblem;
7992
+ this.inputProblem = this.cloneAndCorrectInputProblem(inputProblem);
7803
7993
  this.MAX_ITERATIONS = 1e6;
7804
7994
  this.startTimeOfPhase = {};
7805
7995
  this.endTimeOfPhase = {};
@@ -7807,6 +7997,14 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
7807
7997
  this.firstIterationOfPhase = {};
7808
7998
  }
7809
7999
  currentPipelineStepIndex = 0;
8000
+ cloneAndCorrectInputProblem(original) {
8001
+ const cloned = structuredClone({
8002
+ ...original,
8003
+ _chipObstacleSpatialIndex: void 0
8004
+ });
8005
+ correctPinsInsideChips(cloned);
8006
+ return cloned;
8007
+ }
7810
8008
  _step() {
7811
8009
  const pipelineStepDef = this.pipelineDef[this.currentPipelineStepIndex];
7812
8010
  if (!pipelineStepDef) {
@@ -25,6 +25,7 @@ export class MspConnectionPairSolver extends BaseSolver {
25
25
  globalConnMap: ConnectivityMap
26
26
  queuedDcNetIds: string[]
27
27
  chipMap: Record<string, InputChip>
28
+ maxMspPairDistance: number
28
29
 
29
30
  pinMap: Record<string, InputPin & { chipId: string }>
30
31
  userNetIdByPinId: Record<string, string | undefined>
@@ -33,6 +34,7 @@ export class MspConnectionPairSolver extends BaseSolver {
33
34
  super()
34
35
 
35
36
  this.inputProblem = inputProblem
37
+ this.maxMspPairDistance = inputProblem.maxMspPairDistance ?? 1
36
38
 
37
39
  const { directConnMap, netConnMap } =
38
40
  getConnectivityMapsFromInputProblem(inputProblem)
@@ -112,6 +114,7 @@ export class MspConnectionPairSolver extends BaseSolver {
112
114
 
113
115
  const msp = getOrthogonalMinimumSpanningTree(
114
116
  directlyConnectedPins.map((p) => this.pinMap[p]!),
117
+ { maxDistance: this.maxMspPairDistance },
115
118
  )
116
119
 
117
120
  for (const [pin1, pin2] of msp) {
@@ -18,8 +18,10 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
18
18
  */
19
19
  export function getOrthogonalMinimumSpanningTree(
20
20
  pins: InputPin[],
21
+ opts: { maxDistance?: number } = {},
21
22
  ): Array<[PinId, PinId]> {
22
23
  const n = pins.length
24
+ const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY
23
25
  if (n <= 1) return []
24
26
 
25
27
  // Quick validation (optional; remove if hot path)
@@ -79,7 +81,8 @@ export function getOrthogonalMinimumSpanningTree(
79
81
  // Relax edges from u to all v not yet in the tree
80
82
  for (let v = 0; v < n; v++) {
81
83
  if (!inTree[v]) {
82
- const d = manhattan(pins[u], pins[v])
84
+ const d0 = manhattan(pins[u], pins[v])
85
+ const d = d0 > maxDistance ? Number.POSITIVE_INFINITY : d0
83
86
  if (
84
87
  d < bestDist[v] ||
85
88
  (d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId)
@@ -8,6 +8,7 @@ import type { Point } from "@tscircuit/math-utils"
8
8
  import type { GraphicsObject } from "graphics-debug"
9
9
  import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
10
10
  import { getColorFromString } from "lib/utils/getColorFromString"
11
+ import { getConnectivityMapsFromInputProblem } from "../MspConnectionPairSolver/getConnectivityMapFromInputProblem"
11
12
 
12
13
  /**
13
14
  * A group of traces that have at least one overlapping segment and
@@ -16,7 +17,8 @@ import { getColorFromString } from "lib/utils/getColorFromString"
16
17
  export type OverlappingSameNetTraceGroup = {
17
18
  globalConnNetId: string
18
19
  netId?: string
19
- overlappingTraces: SolvedTracePath
20
+ overlappingTraces?: SolvedTracePath
21
+ portOnlyPinId?: string
20
22
  }
21
23
 
22
24
  export interface NetLabelPlacement {
@@ -76,7 +78,7 @@ export class NetLabelPlacementSolver extends BaseSolver {
76
78
  }
77
79
 
78
80
  computeOverlappingSameNetTraceGroups(): Array<OverlappingSameNetTraceGroup> {
79
- // Group traces by their global connectivity net id.
81
+ // Group existing traces by their global connectivity net id.
80
82
  const byGlobal: Record<string, Array<SolvedTracePath>> = {}
81
83
  for (const trace of Object.values(this.inputTraceMap)) {
82
84
  const key = trace.globalConnNetId
@@ -84,35 +86,120 @@ export class NetLabelPlacementSolver extends BaseSolver {
84
86
  byGlobal[key].push(trace)
85
87
  }
86
88
 
87
- // For each group, pick a representative path (longest by L1 length).
89
+ // Build global connectivity from input so we also consider pins with no traces
90
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
91
+ this.inputProblem,
92
+ )
93
+
94
+ // Map pins to user-provided netIds (if any)
95
+ const userNetIdByPinId: Record<string, string | undefined> = {}
96
+ for (const dc of this.inputProblem.directConnections) {
97
+ if (dc.netId) {
98
+ const [a, b] = dc.pinIds
99
+ userNetIdByPinId[a] = dc.netId
100
+ userNetIdByPinId[b] = dc.netId
101
+ }
102
+ }
103
+ for (const nc of this.inputProblem.netConnections) {
104
+ for (const pid of nc.pinIds) {
105
+ userNetIdByPinId[pid] = nc.netId
106
+ }
107
+ }
108
+
88
109
  const groups: Array<OverlappingSameNetTraceGroup> = []
89
- for (const [globalConnNetId, traces] of Object.entries(byGlobal)) {
90
- if (traces.length === 0) continue
91
- const lengthOf = (path: SolvedTracePath) => {
92
- let sum = 0
93
- const pts = path.tracePath
94
- for (let i = 0; i < pts.length - 1; i++) {
95
- sum +=
96
- Math.abs(pts[i + 1]!.x - pts[i]!.x) +
97
- Math.abs(pts[i + 1]!.y - pts[i]!.y)
110
+
111
+ // Consider every global connectivity net id
112
+ for (const globalConnNetId of Object.keys((netConnMap as any).netMap)) {
113
+ const pinsInNet = netConnMap.getIdsConnectedToNet(
114
+ globalConnNetId,
115
+ ) as string[]
116
+
117
+ // Build adjacency from solved traces (edges)
118
+ const adj: Record<string, Set<string>> = {}
119
+ for (const pid of pinsInNet) adj[pid] = new Set()
120
+ for (const t of byGlobal[globalConnNetId] ?? []) {
121
+ const a = t.pins[0].pinId
122
+ const b = t.pins[1].pinId
123
+ if (adj[a] && adj[b]) {
124
+ adj[a].add(b)
125
+ adj[b].add(a)
98
126
  }
99
- return sum
100
127
  }
101
- let rep = traces[0]!
102
- let repLen = lengthOf(rep)
103
- for (let i = 1; i < traces.length; i++) {
104
- const len = lengthOf(traces[i]!)
105
- if (len > repLen) {
106
- rep = traces[i]!
107
- repLen = len
128
+
129
+ // Find connected components based on trace edges
130
+ const visited = new Set<string>()
131
+ for (const pid of pinsInNet) {
132
+ if (visited.has(pid)) continue
133
+ const stack = [pid]
134
+ const component = new Set<string>()
135
+ visited.add(pid)
136
+ while (stack.length > 0) {
137
+ const u = stack.pop()!
138
+ component.add(u)
139
+ for (const v of adj[u] ?? []) {
140
+ if (!visited.has(v)) {
141
+ visited.add(v)
142
+ stack.push(v)
143
+ }
144
+ }
145
+ }
146
+
147
+ // Collect traces fully inside this component
148
+ const compTraces = (byGlobal[globalConnNetId] ?? []).filter(
149
+ (t) =>
150
+ component.has(t.pins[0].pinId) && component.has(t.pins[1].pinId),
151
+ )
152
+
153
+ if (compTraces.length > 0) {
154
+ // Choose a representative trace (longest by L1 length)
155
+ const lengthOf = (path: SolvedTracePath) => {
156
+ let sum = 0
157
+ const pts = path.tracePath
158
+ for (let i = 0; i < pts.length - 1; i++) {
159
+ sum +=
160
+ Math.abs(pts[i + 1]!.x - pts[i]!.x) +
161
+ Math.abs(pts[i + 1]!.y - pts[i]!.y)
162
+ }
163
+ return sum
164
+ }
165
+ let rep = compTraces[0]!
166
+ let repLen = lengthOf(rep)
167
+ for (let i = 1; i < compTraces.length; i++) {
168
+ const len = lengthOf(compTraces[i]!)
169
+ if (len > repLen) {
170
+ rep = compTraces[i]!
171
+ repLen = len
172
+ }
173
+ }
174
+
175
+ let userNetId = compTraces.find((t) => t.userNetId != null)?.userNetId
176
+ if (!userNetId) {
177
+ for (const p of component) {
178
+ if (userNetIdByPinId[p]) {
179
+ userNetId = userNetIdByPinId[p]
180
+ break
181
+ }
182
+ }
183
+ }
184
+
185
+ groups.push({
186
+ globalConnNetId,
187
+ netId: userNetId,
188
+ overlappingTraces: rep,
189
+ })
190
+ } else {
191
+ // No traces in this component: place label at each pin that has a user net id
192
+ for (const p of component) {
193
+ const userNetId = userNetIdByPinId[p]
194
+ if (!userNetId) continue
195
+ groups.push({
196
+ globalConnNetId,
197
+ netId: userNetId,
198
+ portOnlyPinId: p,
199
+ })
200
+ }
108
201
  }
109
202
  }
110
- const userNetId = traces.find((t) => t.userNetId != null)?.userNetId
111
- groups.push({
112
- globalConnNetId,
113
- netId: userNetId,
114
- overlappingTraces: rep,
115
- })
116
203
  }
117
204
 
118
205
  return groups
@@ -172,6 +172,119 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
172
172
  return
173
173
  }
174
174
 
175
+ // Handle port-only island (no traces) by placing a label at the port
176
+ if (this.overlappingSameNetTraceGroup.portOnlyPinId) {
177
+ const pinId = this.overlappingSameNetTraceGroup.portOnlyPinId
178
+ // Find pin coordinates
179
+ let pin: { x: number; y: number } | null = null
180
+ for (const chip of this.inputProblem.chips) {
181
+ const p = chip.pins.find((pp) => pp.pinId === pinId)
182
+ if (p) {
183
+ pin = { x: p.x, y: p.y }
184
+ break
185
+ }
186
+ }
187
+ if (!pin) {
188
+ this.failed = true
189
+ this.error = `Port-only pin not found: ${pinId}`
190
+ return
191
+ }
192
+
193
+ const orientations =
194
+ this.availableOrientations.length > 0
195
+ ? this.availableOrientations
196
+ : (["x+", "x-", "y+", "y-"] as FacingDirection[])
197
+
198
+ const anchor = { x: pin.x, y: pin.y }
199
+ const outwardOf = (o: FacingDirection) =>
200
+ o === "x+"
201
+ ? { x: 1, y: 0 }
202
+ : o === "x-"
203
+ ? { x: -1, y: 0 }
204
+ : o === "y+"
205
+ ? { x: 0, y: 1 }
206
+ : { x: 0, y: -1 }
207
+
208
+ for (const orientation of orientations) {
209
+ const { width, height } = this.getDimsForOrientation(orientation)
210
+ // Place label fully outside the chip: shift center slightly outward
211
+ const baseCenter = this.getCenterFromAnchor(
212
+ anchor,
213
+ orientation,
214
+ width,
215
+ height,
216
+ )
217
+ const outward = outwardOf(orientation)
218
+ const offset = 1e-3
219
+ const center = {
220
+ x: baseCenter.x + outward.x * offset,
221
+ y: baseCenter.y + outward.y * offset,
222
+ }
223
+ const bounds = this.getRectBounds(center, width, height)
224
+
225
+ // Chip collision check
226
+ const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
227
+ if (chips.length > 0) {
228
+ this.testedCandidates.push({
229
+ center,
230
+ width,
231
+ height,
232
+ bounds,
233
+ anchor,
234
+ orientation,
235
+ status: "chip-collision",
236
+ hostSegIndex: -1,
237
+ })
238
+ continue
239
+ }
240
+
241
+ // Trace collision check
242
+ if (
243
+ this.rectIntersectsAnyTrace(bounds, "" as MspConnectionPairId, -1)
244
+ ) {
245
+ this.testedCandidates.push({
246
+ center,
247
+ width,
248
+ height,
249
+ bounds,
250
+ anchor,
251
+ orientation,
252
+ status: "trace-collision",
253
+ hostSegIndex: -1,
254
+ })
255
+ continue
256
+ }
257
+
258
+ // Found a valid placement
259
+ this.testedCandidates.push({
260
+ center,
261
+ width,
262
+ height,
263
+ bounds,
264
+ anchor,
265
+ orientation,
266
+ status: "ok",
267
+ hostSegIndex: -1,
268
+ })
269
+
270
+ this.netLabelPlacement = {
271
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
272
+ dcConnNetId: undefined,
273
+ orientation,
274
+ anchorPoint: anchor,
275
+ width,
276
+ height,
277
+ center,
278
+ }
279
+ this.solved = true
280
+ return
281
+ }
282
+
283
+ this.failed = true
284
+ this.error = "Could not place net label at port without collisions"
285
+ return
286
+ }
287
+
175
288
  // Prefer starting from the trace connected to the "largest" chip (most pins)
176
289
  const groupId = this.overlappingSameNetTraceGroup.globalConnNetId
177
290
  const chipsById: Record<string, InputChip> = Object.fromEntries(
@@ -218,9 +331,15 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
218
331
  ? hostCandidates.reduce((a, b) => (lengthOf(a) >= lengthOf(b) ? a : b))
219
332
  : this.overlappingSameNetTraceGroup.overlappingTraces
220
333
 
334
+ if (!host) {
335
+ this.failed = true
336
+ this.error = "No host trace found for net label placement"
337
+ return
338
+ }
339
+
221
340
  // Ensure we traverse the host path starting at the segment attached to the largest chip's pin
222
341
  let pts = host.tracePath.slice()
223
- if (largestChipId) {
342
+ if (largestChipId && host) {
224
343
  const largePin =
225
344
  host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1]
226
345
  const d0 =
@@ -312,7 +431,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
312
431
  }
313
432
 
314
433
  // Trace collision check (ignore the host segment)
315
- if (this.rectIntersectsAnyTrace(bounds, host.mspPairId, si)) {
434
+ if (this.rectIntersectsAnyTrace(bounds, host!.mspPairId, si)) {
316
435
  this.testedCandidates.push({
317
436
  center: testCenter,
318
437
  width,
@@ -340,7 +459,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
340
459
 
341
460
  this.netLabelPlacement = {
342
461
  globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
343
- dcConnNetId: host.dcConnNetId,
462
+ dcConnNetId: host!.dcConnNetId,
344
463
  orientation,
345
464
  anchorPoint: anchor,
346
465
  width,
@@ -411,7 +530,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
411
530
 
412
531
  for (const trace of Object.values(this.inputTraceMap)) {
413
532
  if (trace.globalConnNetId !== groupId) continue
414
- const isHost = trace.mspPairId === host.mspPairId
533
+ const isHost = host ? trace.mspPairId === host.mspPairId : false
415
534
  graphics.lines!.push({
416
535
  points: trace.tracePath,
417
536
  strokeColor: isHost ? groupStroke : groupFill,
@@ -12,6 +12,8 @@ import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlap
12
12
  import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
13
13
  import { visualizeInputProblem } from "./visualizeInputProblem"
14
14
  import { GuidelinesSolver } from "../GuidelinesSolver/GuidelinesSolver"
15
+ import { getInputChipBounds } from "../GuidelinesSolver/getInputChipBounds"
16
+ import { correctPinsInsideChips } from "./correctPinsInsideChip"
15
17
 
16
18
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
17
19
  solverName: string
@@ -135,7 +137,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
135
137
 
136
138
  constructor(inputProblem: InputProblem) {
137
139
  super()
138
- this.inputProblem = inputProblem
140
+ this.inputProblem = this.cloneAndCorrectInputProblem(inputProblem)
139
141
  this.MAX_ITERATIONS = 1e6
140
142
  this.startTimeOfPhase = {}
141
143
  this.endTimeOfPhase = {}
@@ -145,6 +147,17 @@ export class SchematicTracePipelineSolver extends BaseSolver {
145
147
 
146
148
  currentPipelineStepIndex = 0
147
149
 
150
+ private cloneAndCorrectInputProblem(original: InputProblem): InputProblem {
151
+ const cloned: InputProblem = structuredClone({
152
+ ...original,
153
+ _chipObstacleSpatialIndex: undefined,
154
+ })
155
+
156
+ correctPinsInsideChips(cloned)
157
+
158
+ return cloned
159
+ }
160
+
148
161
  override _step() {
149
162
  const pipelineStepDef = this.pipelineDef[this.currentPipelineStepIndex]
150
163
  if (!pipelineStepDef) {
@@ -0,0 +1,37 @@
1
+ import type { InputProblem } from "lib/types/InputProblem"
2
+ import { getInputChipBounds } from "../GuidelinesSolver/getInputChipBounds"
3
+
4
+ export const correctPinsInsideChips = (problem: InputProblem) => {
5
+ for (const chip of problem.chips) {
6
+ const bounds = getInputChipBounds(chip)
7
+ for (const pin of chip.pins) {
8
+ const isInside =
9
+ pin.x > bounds.minX &&
10
+ pin.x < bounds.maxX &&
11
+ pin.y > bounds.minY &&
12
+ pin.y < bounds.maxY
13
+
14
+ if (!isInside) continue
15
+
16
+ const distLeft = pin.x - bounds.minX
17
+ const distRight = bounds.maxX - pin.x
18
+ const distBottom = pin.y - bounds.minY
19
+ const distTop = bounds.maxY - pin.y
20
+
21
+ const minDist = Math.min(distLeft, distRight, distBottom, distTop)
22
+
23
+ if (minDist === distLeft) {
24
+ pin.x = bounds.minX
25
+ } else if (minDist === distRight) {
26
+ pin.x = bounds.maxX
27
+ } else if (minDist === distBottom) {
28
+ pin.y = bounds.minY
29
+ } else {
30
+ pin.y = bounds.maxY
31
+ }
32
+
33
+ // Clear any cached facing direction since geometry changed.
34
+ pin._facingDirection = undefined
35
+ }
36
+ }
37
+ }
@@ -35,6 +35,7 @@ export interface InputProblem {
35
35
  netConnections: Array<InputNetConnection>
36
36
 
37
37
  availableNetLabelOrientations: Record<NetId, FacingDirection[]>
38
+ maxMspPairDistance?: number
38
39
 
39
40
  _chipObstacleSpatialIndex?: ChipObstacleSpatialIndex
40
41
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
8
8
  "build": "tsup lib/index.ts --format esm --dts",
9
- "format": "biome format --write ."
9
+ "format": "biome format --write .",
10
+ "format:check": "biome format ."
10
11
  },
11
12
  "devDependencies": {
12
13
  "@biomejs/biome": "^2.2.2",
@@ -99,6 +99,7 @@ const inputProblem: InputProblem = {
99
99
  EN: ["x+", "x-"],
100
100
  GND: ["y+", "y-"],
101
101
  },
102
+ maxMspPairDistance: 2,
102
103
  }
103
104
 
104
105
  export default () => <PipelineDebugger inputProblem={inputProblem} />
@@ -171,6 +171,7 @@ const inputProblem: InputProblem = {
171
171
  pinIds: ["C5.1", "U1.5"],
172
172
  },
173
173
  ],
174
+ maxMspPairDistance: 2,
174
175
  availableNetLabelOrientations: {
175
176
  VSYS: ["y+"],
176
177
  GND: ["y-"],
@@ -0,0 +1,215 @@
1
+ import type { InputProblem } from "lib/index"
2
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
3
+
4
+ const inputProblem: InputProblem = {
5
+ chips: [
6
+ {
7
+ chipId: "schematic_component_0",
8
+ center: {
9
+ x: -3,
10
+ y: 1.5,
11
+ },
12
+ width: 1.0011537820000012,
13
+ height: 0.44923699999999833,
14
+ pins: [
15
+ {
16
+ pinId: "PIN1_PIN2.1",
17
+ x: -3.4724184500000006,
18
+ y: 1.4489565000000004,
19
+ },
20
+ {
21
+ pinId: "PIN1_PIN2.2",
22
+ x: -3.4724184500000006,
23
+ y: 1.4489565000000004,
24
+ },
25
+ {
26
+ pinId: "PIN1_PIN2.3",
27
+ x: -2.5275815499999994,
28
+ y: 1.4485175000000003,
29
+ },
30
+ {
31
+ pinId: "PIN1_PIN2.4",
32
+ x: -2.5275815499999994,
33
+ y: 1.4485175000000003,
34
+ },
35
+ ],
36
+ },
37
+ {
38
+ chipId: "schematic_component_1",
39
+ center: {
40
+ x: 0,
41
+ y: 1.5,
42
+ },
43
+ width: 1.0011537820000012,
44
+ height: 0.44923699999999833,
45
+ pins: [
46
+ {
47
+ pinId: "PIN1_PIN3.1",
48
+ x: -0.4724184500000006,
49
+ y: 1.4489565000000004,
50
+ },
51
+ {
52
+ pinId: "PIN1_PIN3.2",
53
+ x: -0.4724184500000006,
54
+ y: 1.4489565000000004,
55
+ },
56
+ {
57
+ pinId: "PIN1_PIN3.3",
58
+ x: 0.4724184500000006,
59
+ y: 1.4485175000000003,
60
+ },
61
+ {
62
+ pinId: "PIN1_PIN3.4",
63
+ x: 0.4724184500000006,
64
+ y: 1.4485175000000003,
65
+ },
66
+ ],
67
+ },
68
+ {
69
+ chipId: "schematic_component_2",
70
+ center: {
71
+ x: 3,
72
+ y: 1.5,
73
+ },
74
+ width: 1.0011537820000012,
75
+ height: 0.44923699999999833,
76
+ pins: [
77
+ {
78
+ pinId: "PIN1_PIN4.1",
79
+ x: 2.5275815499999994,
80
+ y: 1.4489565000000004,
81
+ },
82
+ {
83
+ pinId: "PIN1_PIN4.2",
84
+ x: 2.5275815499999994,
85
+ y: 1.4489565000000004,
86
+ },
87
+ {
88
+ pinId: "PIN1_PIN4.3",
89
+ x: 3.4724184500000006,
90
+ y: 1.4485175000000003,
91
+ },
92
+ {
93
+ pinId: "PIN1_PIN4.4",
94
+ x: 3.4724184500000006,
95
+ y: 1.4485175000000003,
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ chipId: "schematic_component_3",
101
+ center: {
102
+ x: -3,
103
+ y: -1.5,
104
+ },
105
+ width: 1.0011537820000012,
106
+ height: 0.44923699999999833,
107
+ pins: [
108
+ {
109
+ pinId: "PIN2_PIN3.1",
110
+ x: -3.4724184500000006,
111
+ y: -1.5510434999999996,
112
+ },
113
+ {
114
+ pinId: "PIN2_PIN3.2",
115
+ x: -3.4724184500000006,
116
+ y: -1.5510434999999996,
117
+ },
118
+ {
119
+ pinId: "PIN2_PIN3.3",
120
+ x: -2.5275815499999994,
121
+ y: -1.5514824999999997,
122
+ },
123
+ {
124
+ pinId: "PIN2_PIN3.4",
125
+ x: -2.5275815499999994,
126
+ y: -1.5514824999999997,
127
+ },
128
+ ],
129
+ },
130
+ {
131
+ chipId: "schematic_component_4",
132
+ center: {
133
+ x: 0,
134
+ y: -1.5,
135
+ },
136
+ width: 1.0011537820000012,
137
+ height: 0.44923699999999833,
138
+ pins: [
139
+ {
140
+ pinId: "PIN2_PIN4.1",
141
+ x: -0.4724184500000006,
142
+ y: -1.5510434999999996,
143
+ },
144
+ {
145
+ pinId: "PIN2_PIN4.2",
146
+ x: -0.4724184500000006,
147
+ y: -1.5510434999999996,
148
+ },
149
+ {
150
+ pinId: "PIN2_PIN4.3",
151
+ x: 0.4724184500000006,
152
+ y: -1.5514824999999997,
153
+ },
154
+ {
155
+ pinId: "PIN2_PIN4.4",
156
+ x: 0.4724184500000006,
157
+ y: -1.5514824999999997,
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ chipId: "schematic_component_5",
163
+ center: {
164
+ x: 3,
165
+ y: -1.5,
166
+ },
167
+ width: 1.0011537820000012,
168
+ height: 0.44923699999999833,
169
+ pins: [
170
+ {
171
+ pinId: "PIN3_PIN4.1",
172
+ x: 2.5275815499999994,
173
+ y: -1.5510434999999996,
174
+ },
175
+ {
176
+ pinId: "PIN3_PIN4.2",
177
+ x: 2.5275815499999994,
178
+ y: -1.5510434999999996,
179
+ },
180
+ {
181
+ pinId: "PIN3_PIN4.3",
182
+ x: 3.4724184500000006,
183
+ y: -1.5514824999999997,
184
+ },
185
+ {
186
+ pinId: "PIN3_PIN4.4",
187
+ x: 3.4724184500000006,
188
+ y: -1.5514824999999997,
189
+ },
190
+ ],
191
+ },
192
+ ],
193
+ directConnections: [],
194
+ netConnections: [
195
+ {
196
+ netId: "PIN1",
197
+ pinIds: ["PIN1_PIN2.1", "PIN1_PIN3.1", "PIN1_PIN4.1"],
198
+ },
199
+ {
200
+ netId: "PIN2",
201
+ pinIds: ["PIN1_PIN2.2", "PIN2_PIN3.2", "PIN2_PIN4.2"],
202
+ },
203
+ {
204
+ netId: "PIN3",
205
+ pinIds: ["PIN1_PIN3.3", "PIN2_PIN3.3", "PIN3_PIN4.3"],
206
+ },
207
+ {
208
+ netId: "PIN4",
209
+ pinIds: ["PIN1_PIN4.4", "PIN2_PIN4.4", "PIN3_PIN4.4"],
210
+ },
211
+ ],
212
+ availableNetLabelOrientations: {},
213
+ }
214
+
215
+ export default () => <PipelineDebugger inputProblem={inputProblem} />
package/vite.config.ts CHANGED
@@ -9,4 +9,7 @@ export default defineConfig({
9
9
  tests: path.resolve(__dirname, "tests"),
10
10
  },
11
11
  },
12
+ server: {
13
+ port: 5020,
14
+ },
12
15
  })