@tscircuit/schematic-trace-solver 0.0.7 → 0.0.9

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 (19) hide show
  1. package/dist/index.d.ts +5 -5
  2. package/dist/index.js +488 -344
  3. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +29 -0
  4. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +199 -465
  5. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts +89 -0
  6. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/anchors.ts +11 -0
  7. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions.ts +45 -0
  8. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry.ts +48 -0
  9. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts +64 -0
  10. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +177 -0
  11. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +24 -3
  12. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +38 -3
  13. package/package.json +1 -1
  14. package/site/SchematicTraceLinesSolver/SchematicTraceLinesSolver01.page.tsx +122 -0
  15. package/site/components/GenericSolverDebugger.tsx +16 -0
  16. package/site/examples/example01-basic.page.tsx +2 -2
  17. package/site/examples/example04-single-symbol.page.tsx +46 -0
  18. package/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_repro01.json +88 -0
  19. package/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_repro01.test.ts +16 -0
package/dist/index.js CHANGED
@@ -6694,12 +6694,33 @@ var SchematicTraceSingleLineSolver = class extends BaseSolver {
6694
6694
  return;
6695
6695
  }
6696
6696
  const nextCandidatePath = this.queuedCandidatePaths.shift();
6697
- const obstacleOps = {
6698
- excludeChipIds: [this.pins[0].chipId, this.pins[1].chipId]
6699
- };
6700
6697
  for (let i = 0; i < nextCandidatePath.length - 1; i++) {
6701
6698
  const start = nextCandidatePath[i];
6702
6699
  const end = nextCandidatePath[i + 1];
6700
+ let excludeChipIds = [];
6701
+ const isStartPin = this.pins.some(
6702
+ (pin) => Math.abs(pin.x - start.x) < 1e-10 && Math.abs(pin.y - start.y) < 1e-10
6703
+ );
6704
+ const isEndPin = this.pins.some(
6705
+ (pin) => Math.abs(pin.x - end.x) < 1e-10 && Math.abs(pin.y - end.y) < 1e-10
6706
+ );
6707
+ if (isStartPin) {
6708
+ const startPin = this.pins.find(
6709
+ (pin) => Math.abs(pin.x - start.x) < 1e-10 && Math.abs(pin.y - start.y) < 1e-10
6710
+ );
6711
+ if (startPin && !excludeChipIds.includes(startPin.chipId)) {
6712
+ excludeChipIds.push(startPin.chipId);
6713
+ }
6714
+ }
6715
+ if (isEndPin) {
6716
+ const endPin = this.pins.find(
6717
+ (pin) => Math.abs(pin.x - end.x) < 1e-10 && Math.abs(pin.y - end.y) < 1e-10
6718
+ );
6719
+ if (endPin && !excludeChipIds.includes(endPin.chipId)) {
6720
+ excludeChipIds.push(endPin.chipId);
6721
+ }
6722
+ }
6723
+ const obstacleOps = { excludeChipIds };
6703
6724
  const intersects = this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip(
6704
6725
  [start, end],
6705
6726
  obstacleOps
@@ -6780,6 +6801,7 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
6780
6801
  chipMap;
6781
6802
  currentConnectionPair = null;
6782
6803
  solvedTracePaths = [];
6804
+ failedConnectionPairs = [];
6783
6805
  constructor(params) {
6784
6806
  super();
6785
6807
  this.inputProblem = params.inputProblem;
@@ -6810,9 +6832,14 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
6810
6832
  this.currentConnectionPair = null;
6811
6833
  }
6812
6834
  if (this.activeSubSolver?.failed) {
6813
- this.failed = true;
6814
- this.error = this.activeSubSolver.error;
6815
- return;
6835
+ if (this.currentConnectionPair) {
6836
+ this.failedConnectionPairs.push({
6837
+ ...this.currentConnectionPair,
6838
+ error: this.activeSubSolver.error || void 0
6839
+ });
6840
+ }
6841
+ this.activeSubSolver = null;
6842
+ this.currentConnectionPair = null;
6816
6843
  }
6817
6844
  if (this.activeSubSolver) {
6818
6845
  this.activeSubSolver.step();
@@ -6848,6 +6875,17 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
6848
6875
  strokeColor: "green"
6849
6876
  });
6850
6877
  }
6878
+ for (const pair of this.failedConnectionPairs) {
6879
+ graphics.lines.push({
6880
+ points: [
6881
+ { x: pair.pins[0].x, y: pair.pins[0].y },
6882
+ { x: pair.pins[1].x, y: pair.pins[1].y }
6883
+ ],
6884
+ strokeColor: "red",
6885
+ strokeDash: "4 2",
6886
+ strokeWidth: 4e-3
6887
+ });
6888
+ }
6851
6889
  return graphics;
6852
6890
  }
6853
6891
  };
@@ -7150,9 +7188,292 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
7150
7188
  }
7151
7189
  };
7152
7190
 
7153
- // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts
7191
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry.ts
7154
7192
  var NET_LABEL_HORIZONTAL_WIDTH = 0.4;
7155
7193
  var NET_LABEL_HORIZONTAL_HEIGHT = 0.2;
7194
+ function getDimsForOrientation(orientation) {
7195
+ if (orientation === "y+" || orientation === "y-") {
7196
+ return {
7197
+ width: NET_LABEL_HORIZONTAL_HEIGHT,
7198
+ height: NET_LABEL_HORIZONTAL_WIDTH
7199
+ };
7200
+ }
7201
+ return {
7202
+ width: NET_LABEL_HORIZONTAL_WIDTH,
7203
+ height: NET_LABEL_HORIZONTAL_HEIGHT
7204
+ };
7205
+ }
7206
+ function getCenterFromAnchor(anchor, orientation, width, height) {
7207
+ switch (orientation) {
7208
+ case "x+":
7209
+ return { x: anchor.x + width / 2, y: anchor.y };
7210
+ case "x-":
7211
+ return { x: anchor.x - width / 2, y: anchor.y };
7212
+ case "y+":
7213
+ return { x: anchor.x, y: anchor.y + height / 2 };
7214
+ case "y-":
7215
+ return { x: anchor.x, y: anchor.y - height / 2 };
7216
+ }
7217
+ }
7218
+ function getRectBounds(center, w, h) {
7219
+ return {
7220
+ minX: center.x - w / 2,
7221
+ minY: center.y - h / 2,
7222
+ maxX: center.x + w / 2,
7223
+ maxY: center.y + h / 2
7224
+ };
7225
+ }
7226
+
7227
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions.ts
7228
+ function segmentIntersectsRect(p1, p2, rect, EPS = 1e-9) {
7229
+ const isVert = Math.abs(p1.x - p2.x) < EPS;
7230
+ const isHorz = Math.abs(p1.y - p2.y) < EPS;
7231
+ if (!isVert && !isHorz) return false;
7232
+ if (isVert) {
7233
+ const x = p1.x;
7234
+ if (x < rect.minX - EPS || x > rect.maxX + EPS) return false;
7235
+ const segMinY = Math.min(p1.y, p2.y);
7236
+ const segMaxY = Math.max(p1.y, p2.y);
7237
+ const overlap = Math.min(segMaxY, rect.maxY) - Math.max(segMinY, rect.minY);
7238
+ return overlap > EPS;
7239
+ } else {
7240
+ const y = p1.y;
7241
+ if (y < rect.minY - EPS || y > rect.maxY + EPS) return false;
7242
+ const segMinX = Math.min(p1.x, p2.x);
7243
+ const segMaxX = Math.max(p1.x, p2.x);
7244
+ const overlap = Math.min(segMaxX, rect.maxX) - Math.max(segMinX, rect.minX);
7245
+ return overlap > EPS;
7246
+ }
7247
+ }
7248
+ function rectIntersectsAnyTrace(bounds, inputTraceMap, hostPathId, hostSegIndex) {
7249
+ for (const [pairId, solved] of Object.entries(inputTraceMap)) {
7250
+ const pts = solved.tracePath;
7251
+ for (let i = 0; i < pts.length - 1; i++) {
7252
+ if (pairId === hostPathId && i === hostSegIndex) continue;
7253
+ if (segmentIntersectsRect(pts[i], pts[i + 1], bounds)) return true;
7254
+ }
7255
+ }
7256
+ return false;
7257
+ }
7258
+
7259
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts
7260
+ function lengthOfTrace(path) {
7261
+ let sum = 0;
7262
+ const pts = path.tracePath;
7263
+ for (let i = 0; i < pts.length - 1; i++) {
7264
+ sum += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y);
7265
+ }
7266
+ return sum;
7267
+ }
7268
+ function chooseHostTraceForGroup(params) {
7269
+ const { inputProblem, inputTraceMap, globalConnNetId, fallbackTrace } = params;
7270
+ const chipsById = Object.fromEntries(
7271
+ inputProblem.chips.map((c) => [c.chipId, c])
7272
+ );
7273
+ const groupTraces = Object.values(inputTraceMap).filter(
7274
+ (t) => t.globalConnNetId === globalConnNetId
7275
+ );
7276
+ const chipIdsInGroup = /* @__PURE__ */ new Set();
7277
+ for (const t of groupTraces) {
7278
+ chipIdsInGroup.add(t.pins[0].chipId);
7279
+ chipIdsInGroup.add(t.pins[1].chipId);
7280
+ }
7281
+ let largestChipId = null;
7282
+ let largestPinCount = -1;
7283
+ for (const id of chipIdsInGroup) {
7284
+ const chip = chipsById[id];
7285
+ const count = chip?.pins?.length ?? 0;
7286
+ if (count > largestPinCount) {
7287
+ largestPinCount = count;
7288
+ largestChipId = id;
7289
+ }
7290
+ }
7291
+ const hostCandidates = largestChipId == null ? [] : groupTraces.filter(
7292
+ (t) => t.pins[0].chipId === largestChipId || t.pins[1].chipId === largestChipId
7293
+ );
7294
+ if (hostCandidates.length > 0) {
7295
+ return hostCandidates.reduce(
7296
+ (a, b) => lengthOfTrace(a) >= lengthOfTrace(b) ? a : b
7297
+ );
7298
+ }
7299
+ return fallbackTrace;
7300
+ }
7301
+
7302
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/anchors.ts
7303
+ function anchorsForSegment(a, b) {
7304
+ return [
7305
+ { x: a.x, y: a.y },
7306
+ { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
7307
+ { x: b.x, y: b.y }
7308
+ ];
7309
+ }
7310
+
7311
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts
7312
+ function solveNetLabelPlacementForPortOnlyPin(params) {
7313
+ const {
7314
+ inputProblem,
7315
+ inputTraceMap,
7316
+ chipObstacleSpatialIndex,
7317
+ overlappingSameNetTraceGroup,
7318
+ availableOrientations
7319
+ } = params;
7320
+ const pinId = overlappingSameNetTraceGroup.portOnlyPinId;
7321
+ if (!pinId) {
7322
+ return {
7323
+ placement: null,
7324
+ testedCandidates: [],
7325
+ error: "No portOnlyPinId provided"
7326
+ };
7327
+ }
7328
+ let pin = null;
7329
+ for (const chip of inputProblem.chips) {
7330
+ const p = chip.pins.find((pp) => pp.pinId === pinId);
7331
+ if (p) {
7332
+ pin = { x: p.x, y: p.y };
7333
+ break;
7334
+ }
7335
+ }
7336
+ if (!pin) {
7337
+ return {
7338
+ placement: null,
7339
+ testedCandidates: [],
7340
+ error: `Port-only pin not found: ${pinId}`
7341
+ };
7342
+ }
7343
+ const orientations = availableOrientations.length > 0 ? availableOrientations : ["x+", "x-", "y+", "y-"];
7344
+ const anchor = { x: pin.x, y: pin.y };
7345
+ 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 };
7346
+ const testedCandidates = [];
7347
+ for (const orientation of orientations) {
7348
+ const { width, height } = getDimsForOrientation(orientation);
7349
+ const baseCenter = getCenterFromAnchor(anchor, orientation, width, height);
7350
+ const outward = outwardOf(orientation);
7351
+ const offset = 1e-3;
7352
+ const center = {
7353
+ x: baseCenter.x + outward.x * offset,
7354
+ y: baseCenter.y + outward.y * offset
7355
+ };
7356
+ const bounds = getRectBounds(center, width, height);
7357
+ const chips = chipObstacleSpatialIndex.getChipsInBounds(bounds);
7358
+ if (chips.length > 0) {
7359
+ testedCandidates.push({
7360
+ center,
7361
+ width,
7362
+ height,
7363
+ bounds,
7364
+ anchor,
7365
+ orientation,
7366
+ status: "chip-collision",
7367
+ hostSegIndex: -1
7368
+ });
7369
+ continue;
7370
+ }
7371
+ if (rectIntersectsAnyTrace(
7372
+ bounds,
7373
+ inputTraceMap,
7374
+ "",
7375
+ -1
7376
+ )) {
7377
+ testedCandidates.push({
7378
+ center,
7379
+ width,
7380
+ height,
7381
+ bounds,
7382
+ anchor,
7383
+ orientation,
7384
+ status: "trace-collision",
7385
+ hostSegIndex: -1
7386
+ });
7387
+ continue;
7388
+ }
7389
+ testedCandidates.push({
7390
+ center,
7391
+ width,
7392
+ height,
7393
+ bounds,
7394
+ anchor,
7395
+ orientation,
7396
+ status: "ok",
7397
+ hostSegIndex: -1
7398
+ });
7399
+ const placement = {
7400
+ globalConnNetId: overlappingSameNetTraceGroup.globalConnNetId,
7401
+ dcConnNetId: void 0,
7402
+ orientation,
7403
+ anchorPoint: anchor,
7404
+ width,
7405
+ height,
7406
+ center
7407
+ };
7408
+ return { placement, testedCandidates };
7409
+ }
7410
+ return {
7411
+ placement: null,
7412
+ testedCandidates,
7413
+ error: "Could not place net label at port without collisions"
7414
+ };
7415
+ }
7416
+
7417
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts
7418
+ function visualizeSingleNetLabelPlacementSolver(solver) {
7419
+ const graphics = visualizeInputProblem(solver.inputProblem);
7420
+ const groupId = solver.overlappingSameNetTraceGroup.globalConnNetId;
7421
+ const host = chooseHostTraceForGroup({
7422
+ inputProblem: solver.inputProblem,
7423
+ inputTraceMap: solver.inputTraceMap,
7424
+ globalConnNetId: groupId,
7425
+ fallbackTrace: solver.overlappingSameNetTraceGroup.overlappingTraces
7426
+ });
7427
+ const groupStroke = getColorFromString(groupId, 0.9);
7428
+ const groupFill = getColorFromString(groupId, 0.5);
7429
+ for (const trace of Object.values(solver.inputTraceMap)) {
7430
+ if (trace.globalConnNetId !== groupId) continue;
7431
+ const isHost = host ? trace.mspPairId === host.mspPairId : false;
7432
+ graphics.lines.push({
7433
+ points: trace.tracePath,
7434
+ strokeColor: isHost ? groupStroke : groupFill,
7435
+ strokeWidth: isHost ? 6e-3 : 3e-3,
7436
+ strokeDash: isHost ? void 0 : "4 2"
7437
+ });
7438
+ }
7439
+ for (const c of solver.testedCandidates) {
7440
+ const fill = c.status === "ok" ? "rgba(0, 180, 0, 0.25)" : c.status === "chip-collision" ? "rgba(220, 0, 0, 0.25)" : c.status === "trace-collision" ? "rgba(220, 140, 0, 0.25)" : "rgba(120, 120, 120, 0.15)";
7441
+ const stroke = c.status === "ok" ? "green" : c.status === "chip-collision" ? "red" : c.status === "trace-collision" ? "orange" : "gray";
7442
+ graphics.rects.push({
7443
+ center: {
7444
+ x: (c.bounds.minX + c.bounds.maxX) / 2,
7445
+ y: (c.bounds.minY + c.bounds.maxY) / 2
7446
+ },
7447
+ width: c.width,
7448
+ height: c.height,
7449
+ fill,
7450
+ strokeColor: stroke
7451
+ });
7452
+ graphics.points.push({
7453
+ x: c.anchor.x,
7454
+ y: c.anchor.y,
7455
+ color: stroke
7456
+ });
7457
+ }
7458
+ if (solver.netLabelPlacement) {
7459
+ const p = solver.netLabelPlacement;
7460
+ graphics.rects.push({
7461
+ center: p.center,
7462
+ width: p.width,
7463
+ height: p.height,
7464
+ fill: "rgba(0, 128, 255, 0.35)",
7465
+ strokeColor: "blue"
7466
+ });
7467
+ graphics.points.push({
7468
+ x: p.anchorPoint.x,
7469
+ y: p.anchorPoint.y,
7470
+ color: "blue"
7471
+ });
7472
+ }
7473
+ return graphics;
7474
+ }
7475
+
7476
+ // lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts
7156
7477
  var SingleNetLabelPlacementSolver = class extends BaseSolver {
7157
7478
  inputProblem;
7158
7479
  inputTraceMap;
@@ -7169,260 +7490,127 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7169
7490
  this.availableOrientations = params.availableOrientations;
7170
7491
  this.chipObstacleSpatialIndex = params.inputProblem._chipObstacleSpatialIndex ?? new ChipObstacleSpatialIndex(params.inputProblem.chips);
7171
7492
  }
7172
- getDimsForOrientation(orientation) {
7173
- if (orientation === "y+" || orientation === "y-") {
7174
- return {
7175
- width: NET_LABEL_HORIZONTAL_HEIGHT,
7176
- height: NET_LABEL_HORIZONTAL_WIDTH
7177
- };
7178
- }
7179
- return {
7180
- width: NET_LABEL_HORIZONTAL_WIDTH,
7181
- height: NET_LABEL_HORIZONTAL_HEIGHT
7182
- };
7183
- }
7184
- getCenterFromAnchor(anchor, orientation, width, height) {
7185
- switch (orientation) {
7186
- case "x+":
7187
- return { x: anchor.x + width / 2, y: anchor.y };
7188
- case "x-":
7189
- return { x: anchor.x - width / 2, y: anchor.y };
7190
- case "y+":
7191
- return { x: anchor.x, y: anchor.y + height / 2 };
7192
- case "y-":
7193
- return { x: anchor.x, y: anchor.y - height / 2 };
7194
- }
7195
- }
7196
- getRectBounds(center, w, h) {
7197
- return {
7198
- minX: center.x - w / 2,
7199
- minY: center.y - h / 2,
7200
- maxX: center.x + w / 2,
7201
- maxY: center.y + h / 2
7202
- };
7203
- }
7204
- segmentIntersectsRect(p1, p2, rect, EPS = 1e-9) {
7205
- const isVert = Math.abs(p1.x - p2.x) < EPS;
7206
- const isHorz = Math.abs(p1.y - p2.y) < EPS;
7207
- if (!isVert && !isHorz) return false;
7208
- if (isVert) {
7209
- const x = p1.x;
7210
- if (x < rect.minX - EPS || x > rect.maxX + EPS) return false;
7211
- const segMinY = Math.min(p1.y, p2.y);
7212
- const segMaxY = Math.max(p1.y, p2.y);
7213
- const overlap = Math.min(segMaxY, rect.maxY) - Math.max(segMinY, rect.minY);
7214
- return overlap > EPS;
7215
- } else {
7216
- const y = p1.y;
7217
- if (y < rect.minY - EPS || y > rect.maxY + EPS) return false;
7218
- const segMinX = Math.min(p1.x, p2.x);
7219
- const segMaxX = Math.max(p1.x, p2.x);
7220
- const overlap = Math.min(segMaxX, rect.maxX) - Math.max(segMinX, rect.minX);
7221
- return overlap > EPS;
7222
- }
7223
- }
7224
- rectIntersectsAnyTrace(bounds, hostPathId, hostSegIndex) {
7225
- for (const [pairId, solved] of Object.entries(this.inputTraceMap)) {
7226
- const pts = solved.tracePath;
7227
- for (let i = 0; i < pts.length - 1; i++) {
7228
- if (pairId === hostPathId && i === hostSegIndex) continue;
7229
- if (this.segmentIntersectsRect(pts[i], pts[i + 1], bounds))
7230
- return true;
7231
- }
7232
- }
7233
- return false;
7234
- }
7235
7493
  _step() {
7236
7494
  if (this.netLabelPlacement) {
7237
7495
  this.solved = true;
7238
7496
  return;
7239
7497
  }
7240
7498
  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
- };
7499
+ const res = solveNetLabelPlacementForPortOnlyPin({
7500
+ inputProblem: this.inputProblem,
7501
+ inputTraceMap: this.inputTraceMap,
7502
+ chipObstacleSpatialIndex: this.chipObstacleSpatialIndex,
7503
+ overlappingSameNetTraceGroup: this.overlappingSameNetTraceGroup,
7504
+ availableOrientations: this.availableOrientations
7505
+ });
7506
+ this.testedCandidates.push(...res.testedCandidates);
7507
+ if (res.placement) {
7508
+ this.netLabelPlacement = res.placement;
7319
7509
  this.solved = true;
7320
7510
  return;
7321
7511
  }
7322
7512
  this.failed = true;
7323
- this.error = "Could not place net label at port without collisions";
7513
+ this.error = res.error ?? "Could not place net label at port without collisions";
7324
7514
  return;
7325
7515
  }
7326
7516
  const groupId = this.overlappingSameNetTraceGroup.globalConnNetId;
7327
- const chipsById = Object.fromEntries(
7328
- this.inputProblem.chips.map((c) => [c.chipId, c])
7329
- );
7330
- const groupTraces = Object.values(this.inputTraceMap).filter(
7331
- (t) => t.globalConnNetId === groupId
7332
- );
7333
- const chipIdsInGroup = /* @__PURE__ */ new Set();
7334
- for (const t of groupTraces) {
7335
- chipIdsInGroup.add(t.pins[0].chipId);
7336
- chipIdsInGroup.add(t.pins[1].chipId);
7337
- }
7338
- let largestChipId = null;
7339
- let largestPinCount = -1;
7340
- for (const id of chipIdsInGroup) {
7341
- const chip = chipsById[id];
7342
- const count = chip?.pins?.length ?? 0;
7343
- if (count > largestPinCount) {
7344
- largestPinCount = count;
7345
- largestChipId = id;
7346
- }
7347
- }
7348
- const lengthOf = (path) => {
7349
- let sum = 0;
7350
- const pts2 = path.tracePath;
7351
- for (let i = 0; i < pts2.length - 1; i++) {
7352
- sum += Math.abs(pts2[i + 1].x - pts2[i].x) + Math.abs(pts2[i + 1].y - pts2[i].y);
7353
- }
7354
- return sum;
7355
- };
7356
- const hostCandidates = largestChipId == null ? [] : groupTraces.filter(
7357
- (t) => t.pins[0].chipId === largestChipId || t.pins[1].chipId === largestChipId
7358
- );
7359
- let host = hostCandidates.length > 0 ? hostCandidates.reduce((a, b) => lengthOf(a) >= lengthOf(b) ? a : b) : this.overlappingSameNetTraceGroup.overlappingTraces;
7517
+ let host = chooseHostTraceForGroup({
7518
+ inputProblem: this.inputProblem,
7519
+ inputTraceMap: this.inputTraceMap,
7520
+ globalConnNetId: groupId,
7521
+ fallbackTrace: this.overlappingSameNetTraceGroup.overlappingTraces
7522
+ });
7360
7523
  if (!host) {
7361
7524
  this.failed = true;
7362
7525
  this.error = "No host trace found for net label placement";
7363
7526
  return;
7364
7527
  }
7365
- let pts = host.tracePath.slice();
7366
- if (largestChipId && host) {
7367
- const largePin = host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1];
7368
- const d0 = Math.abs(pts[0].x - largePin.x) + Math.abs(pts[0].y - largePin.y);
7369
- const dL = Math.abs(pts[pts.length - 1].x - largePin.x) + Math.abs(pts[pts.length - 1].y - largePin.y);
7370
- if (d0 > dL) {
7371
- pts = pts.slice().reverse();
7372
- }
7373
- }
7528
+ const tracesToScanBase = Object.values(this.inputTraceMap).filter(
7529
+ (t) => t.globalConnNetId === groupId
7530
+ );
7531
+ const tracesToScan = this.availableOrientations.length === 1 ? [
7532
+ host,
7533
+ ...tracesToScanBase.filter((t) => t.mspPairId !== host.mspPairId)
7534
+ ] : [host];
7374
7535
  const orientations = this.availableOrientations.length > 0 ? this.availableOrientations : ["x+", "x-", "y+", "y-"];
7536
+ const singleOrientationMode = this.availableOrientations.length === 1;
7537
+ const scoreFor = (orientation, anchor) => {
7538
+ switch (orientation) {
7539
+ case "y+":
7540
+ return anchor.y;
7541
+ case "y-":
7542
+ return -anchor.y;
7543
+ case "x+":
7544
+ return anchor.x;
7545
+ case "x-":
7546
+ return -anchor.x;
7547
+ }
7548
+ };
7549
+ let bestCandidate = null;
7550
+ let bestScore = -Infinity;
7375
7551
  const EPS = 1e-6;
7376
- const anchorsForSegment = (a, b) => {
7377
- return [
7378
- { x: a.x, y: a.y },
7379
- { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
7380
- { x: b.x, y: b.y }
7381
- ];
7382
- };
7383
- for (let si = 0; si < pts.length - 1; si++) {
7384
- const a = pts[si];
7385
- const b = pts[si + 1];
7386
- const isH = Math.abs(a.y - b.y) < EPS;
7387
- const isV = Math.abs(a.x - b.x) < EPS;
7388
- if (!isH && !isV) continue;
7389
- const segmentAllowed = isH ? ["y+", "y-"] : ["x+", "x-"];
7390
- const candidateOrients = orientations.filter(
7391
- (o) => segmentAllowed.includes(o)
7392
- );
7393
- if (candidateOrients.length === 0) continue;
7394
- const anchors = anchorsForSegment(a, b);
7395
- for (const anchor of anchors) {
7396
- for (const orientation of candidateOrients) {
7397
- const { width, height } = this.getDimsForOrientation(orientation);
7398
- const center = this.getCenterFromAnchor(
7399
- anchor,
7400
- orientation,
7401
- width,
7402
- height
7403
- );
7404
- const outward = orientation === "x+" ? { x: 1, y: 0 } : orientation === "x-" ? { x: -1, y: 0 } : orientation === "y+" ? { x: 0, y: 1 } : { x: 0, y: -1 };
7405
- const offset = 1e-4;
7406
- const testCenter = {
7407
- x: center.x + outward.x * offset,
7408
- y: center.y + outward.y * offset
7409
- };
7410
- const bounds = this.getRectBounds(testCenter, width, height);
7411
- const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
7412
- if (chips.length > 0) {
7413
- this.testedCandidates.push({
7414
- center: testCenter,
7415
- width,
7416
- height,
7417
- bounds,
7552
+ for (const curr of tracesToScan) {
7553
+ const pts = curr.tracePath.slice();
7554
+ for (let si = 0; si < pts.length - 1; si++) {
7555
+ const a = pts[si];
7556
+ const b = pts[si + 1];
7557
+ const isH = Math.abs(a.y - b.y) < EPS;
7558
+ const isV = Math.abs(a.x - b.x) < EPS;
7559
+ if (!isH && !isV) continue;
7560
+ const segmentAllowed = isH ? ["y+", "y-"] : ["x+", "x-"];
7561
+ const candidateOrients = orientations.filter(
7562
+ (o) => segmentAllowed.includes(o)
7563
+ );
7564
+ if (candidateOrients.length === 0) continue;
7565
+ const anchors = anchorsForSegment(a, b);
7566
+ for (const anchor of anchors) {
7567
+ for (const orientation of candidateOrients) {
7568
+ const { width, height } = getDimsForOrientation(orientation);
7569
+ const center = getCenterFromAnchor(
7418
7570
  anchor,
7419
7571
  orientation,
7420
- status: "chip-collision",
7421
- hostSegIndex: si
7422
- });
7423
- continue;
7424
- }
7425
- if (this.rectIntersectsAnyTrace(bounds, host.mspPairId, si)) {
7572
+ width,
7573
+ height
7574
+ );
7575
+ const outward = orientation === "x+" ? { x: 1, y: 0 } : orientation === "x-" ? { x: -1, y: 0 } : orientation === "y+" ? { x: 0, y: 1 } : { x: 0, y: -1 };
7576
+ const offset = 1e-4;
7577
+ const testCenter = {
7578
+ x: center.x + outward.x * offset,
7579
+ y: center.y + outward.y * offset
7580
+ };
7581
+ const bounds = getRectBounds(testCenter, width, height);
7582
+ const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
7583
+ if (chips.length > 0) {
7584
+ this.testedCandidates.push({
7585
+ center: testCenter,
7586
+ width,
7587
+ height,
7588
+ bounds,
7589
+ anchor,
7590
+ orientation,
7591
+ status: "chip-collision",
7592
+ hostSegIndex: si
7593
+ });
7594
+ continue;
7595
+ }
7596
+ if (rectIntersectsAnyTrace(
7597
+ bounds,
7598
+ this.inputTraceMap,
7599
+ curr.mspPairId,
7600
+ si
7601
+ )) {
7602
+ this.testedCandidates.push({
7603
+ center: testCenter,
7604
+ width,
7605
+ height,
7606
+ bounds,
7607
+ anchor,
7608
+ orientation,
7609
+ status: "trace-collision",
7610
+ hostSegIndex: si
7611
+ });
7612
+ continue;
7613
+ }
7426
7614
  this.testedCandidates.push({
7427
7615
  center: testCenter,
7428
7616
  width,
@@ -7430,121 +7618,58 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
7430
7618
  bounds,
7431
7619
  anchor,
7432
7620
  orientation,
7433
- status: "trace-collision",
7621
+ status: "ok",
7434
7622
  hostSegIndex: si
7435
7623
  });
7436
- continue;
7624
+ if (singleOrientationMode) {
7625
+ const s = scoreFor(orientation, anchor);
7626
+ if (s > bestScore + 1e-9) {
7627
+ bestScore = s;
7628
+ bestCandidate = {
7629
+ anchor,
7630
+ orientation,
7631
+ width,
7632
+ height,
7633
+ center,
7634
+ hostSegIndex: si,
7635
+ dcConnNetId: curr.dcConnNetId
7636
+ };
7637
+ }
7638
+ continue;
7639
+ }
7640
+ this.netLabelPlacement = {
7641
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
7642
+ dcConnNetId: curr.dcConnNetId,
7643
+ orientation,
7644
+ anchorPoint: anchor,
7645
+ width,
7646
+ height,
7647
+ center
7648
+ };
7649
+ this.solved = true;
7650
+ return;
7437
7651
  }
7438
- this.testedCandidates.push({
7439
- center: testCenter,
7440
- width,
7441
- height,
7442
- bounds,
7443
- anchor,
7444
- orientation,
7445
- status: "ok",
7446
- hostSegIndex: si
7447
- });
7448
- this.netLabelPlacement = {
7449
- globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
7450
- dcConnNetId: host.dcConnNetId,
7451
- orientation,
7452
- anchorPoint: anchor,
7453
- width,
7454
- height,
7455
- center
7456
- };
7457
- this.solved = true;
7458
- return;
7459
7652
  }
7460
7653
  }
7461
7654
  }
7655
+ if (singleOrientationMode && bestCandidate) {
7656
+ this.netLabelPlacement = {
7657
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
7658
+ dcConnNetId: bestCandidate.dcConnNetId,
7659
+ orientation: bestCandidate.orientation,
7660
+ anchorPoint: bestCandidate.anchor,
7661
+ width: bestCandidate.width,
7662
+ height: bestCandidate.height,
7663
+ center: bestCandidate.center
7664
+ };
7665
+ this.solved = true;
7666
+ return;
7667
+ }
7462
7668
  this.failed = true;
7463
7669
  this.error = "Could not place net label without collisions";
7464
7670
  }
7465
7671
  visualize() {
7466
- const graphics = visualizeInputProblem(this.inputProblem);
7467
- const groupId = this.overlappingSameNetTraceGroup.globalConnNetId;
7468
- const chipsById = Object.fromEntries(
7469
- this.inputProblem.chips.map((c) => [c.chipId, c])
7470
- );
7471
- const groupTraces = Object.values(this.inputTraceMap).filter(
7472
- (t) => t.globalConnNetId === groupId
7473
- );
7474
- const chipIdsInGroup = /* @__PURE__ */ new Set();
7475
- for (const t of groupTraces) {
7476
- chipIdsInGroup.add(t.pins[0].chipId);
7477
- chipIdsInGroup.add(t.pins[1].chipId);
7478
- }
7479
- let largestChipId = null;
7480
- let largestPinCount = -1;
7481
- for (const id of chipIdsInGroup) {
7482
- const chip = chipsById[id];
7483
- const count = chip?.pins?.length ?? 0;
7484
- if (count > largestPinCount) {
7485
- largestPinCount = count;
7486
- largestChipId = id;
7487
- }
7488
- }
7489
- const lengthOf = (path) => {
7490
- let sum = 0;
7491
- const pts = path.tracePath;
7492
- for (let i = 0; i < pts.length - 1; i++) {
7493
- sum += Math.abs(pts[i + 1].x - pts[i].x) + Math.abs(pts[i + 1].y - pts[i].y);
7494
- }
7495
- return sum;
7496
- };
7497
- const hostCandidates = largestChipId == null ? [] : groupTraces.filter(
7498
- (t) => t.pins[0].chipId === largestChipId || t.pins[1].chipId === largestChipId
7499
- );
7500
- const host = hostCandidates.length > 0 ? hostCandidates.reduce((a, b) => lengthOf(a) >= lengthOf(b) ? a : b) : this.overlappingSameNetTraceGroup.overlappingTraces;
7501
- const groupStroke = getColorFromString(groupId, 0.9);
7502
- const groupFill = getColorFromString(groupId, 0.5);
7503
- for (const trace of Object.values(this.inputTraceMap)) {
7504
- if (trace.globalConnNetId !== groupId) continue;
7505
- const isHost = host ? trace.mspPairId === host.mspPairId : false;
7506
- graphics.lines.push({
7507
- points: trace.tracePath,
7508
- strokeColor: isHost ? groupStroke : groupFill,
7509
- strokeWidth: isHost ? 6e-3 : 3e-3,
7510
- strokeDash: isHost ? void 0 : "4 2"
7511
- });
7512
- }
7513
- for (const c of this.testedCandidates) {
7514
- const fill = c.status === "ok" ? "rgba(0, 180, 0, 0.25)" : c.status === "chip-collision" ? "rgba(220, 0, 0, 0.25)" : c.status === "trace-collision" ? "rgba(220, 140, 0, 0.25)" : "rgba(120, 120, 120, 0.15)";
7515
- const stroke = c.status === "ok" ? "green" : c.status === "chip-collision" ? "red" : c.status === "trace-collision" ? "orange" : "gray";
7516
- graphics.rects.push({
7517
- center: {
7518
- x: (c.bounds.minX + c.bounds.maxX) / 2,
7519
- y: (c.bounds.minY + c.bounds.maxY) / 2
7520
- },
7521
- width: c.width,
7522
- height: c.height,
7523
- fill,
7524
- strokeColor: stroke
7525
- });
7526
- graphics.points.push({
7527
- x: c.anchor.x,
7528
- y: c.anchor.y,
7529
- color: stroke
7530
- });
7531
- }
7532
- if (this.netLabelPlacement) {
7533
- const p = this.netLabelPlacement;
7534
- graphics.rects.push({
7535
- center: p.center,
7536
- width: p.width,
7537
- height: p.height,
7538
- fill: "rgba(0, 128, 255, 0.35)",
7539
- strokeColor: "blue"
7540
- });
7541
- graphics.points.push({
7542
- x: p.anchorPoint.x,
7543
- y: p.anchorPoint.y,
7544
- color: "blue"
7545
- });
7546
- }
7547
- return graphics;
7672
+ return visualizeSingleNetLabelPlacementSolver(this);
7548
7673
  }
7549
7674
  };
7550
7675
 
@@ -7555,6 +7680,8 @@ var NetLabelPlacementSolver = class extends BaseSolver {
7555
7680
  overlappingSameNetTraceGroups;
7556
7681
  queuedOverlappingSameNetTraceGroups;
7557
7682
  netLabelPlacements = [];
7683
+ currentGroup = null;
7684
+ triedAnyOrientationFallbackForCurrentGroup = false;
7558
7685
  constructor(params) {
7559
7686
  super();
7560
7687
  this.inputProblem = params.inputProblem;
@@ -7672,9 +7799,24 @@ var NetLabelPlacementSolver = class extends BaseSolver {
7672
7799
  if (this.activeSubSolver?.solved) {
7673
7800
  this.netLabelPlacements.push(this.activeSubSolver.netLabelPlacement);
7674
7801
  this.activeSubSolver = null;
7802
+ this.currentGroup = null;
7803
+ this.triedAnyOrientationFallbackForCurrentGroup = false;
7675
7804
  return;
7676
7805
  }
7677
7806
  if (this.activeSubSolver?.failed) {
7807
+ const fullOrients = ["x+", "x-", "y+", "y-"];
7808
+ const currOrients = this.activeSubSolver.availableOrientations;
7809
+ const isAlreadyFull = currOrients.length === 4 && fullOrients.every((o) => currOrients.includes(o));
7810
+ if (!this.triedAnyOrientationFallbackForCurrentGroup && !isAlreadyFull && this.currentGroup) {
7811
+ this.triedAnyOrientationFallbackForCurrentGroup = true;
7812
+ this.activeSubSolver = new SingleNetLabelPlacementSolver({
7813
+ inputProblem: this.inputProblem,
7814
+ inputTraceMap: this.inputTraceMap,
7815
+ overlappingSameNetTraceGroup: this.currentGroup,
7816
+ availableOrientations: fullOrients
7817
+ });
7818
+ return;
7819
+ }
7678
7820
  this.failed = true;
7679
7821
  this.error = this.activeSubSolver.error;
7680
7822
  return;
@@ -7689,6 +7831,8 @@ var NetLabelPlacementSolver = class extends BaseSolver {
7689
7831
  return;
7690
7832
  }
7691
7833
  const netId = nextOverlappingSameNetTraceGroup.netId ?? nextOverlappingSameNetTraceGroup.globalConnNetId;
7834
+ this.currentGroup = nextOverlappingSameNetTraceGroup;
7835
+ this.triedAnyOrientationFallbackForCurrentGroup = false;
7692
7836
  this.activeSubSolver = new SingleNetLabelPlacementSolver({
7693
7837
  inputProblem: this.inputProblem,
7694
7838
  inputTraceMap: this.inputTraceMap,