@tscircuit/schematic-trace-solver 0.0.147 → 0.0.149

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
@@ -1186,6 +1186,8 @@ declare const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
1186
1186
  * Gap between the trace and the near edge of the inline label text.
1187
1187
  */
1188
1188
  declare const INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
1189
+ /** Clearance between a generated terminal stub and an unrelated trace. */
1190
+ declare const INLINE_NET_LABEL_STUB_TRACE_CLEARANCE = 0.05;
1189
1191
  /**
1190
1192
  * Largest perpendicular deviation a route may have and still be labeled as one
1191
1193
  * span. A route that is straight except for elbow jogs up to this size reads
@@ -1361,4 +1363,4 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
1361
1363
  preview(): GraphicsObject;
1362
1364
  }
1363
1365
 
1364
- export { type ChipId, DEFAULT_INLINE_NET_LABEL_HEIGHT, INLINE_NET_LABEL_MAX_SPAN_JOG, INLINE_NET_LABEL_TRACE_MARGIN, type InlineNetLabelPlacement, InlineNetLabelSolver, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem, type NetId, type NetLabelPlacement, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes, estimateInlineNetLabelWidth };
1366
+ export { type ChipId, DEFAULT_INLINE_NET_LABEL_HEIGHT, INLINE_NET_LABEL_MAX_SPAN_JOG, INLINE_NET_LABEL_STUB_TRACE_CLEARANCE, INLINE_NET_LABEL_TRACE_MARGIN, type InlineNetLabelPlacement, InlineNetLabelSolver, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem, type NetId, type NetLabelPlacement, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes, estimateInlineNetLabelWidth };
package/dist/index.js CHANGED
@@ -6352,6 +6352,51 @@ var generateLShapeRerouteCandidates = ({
6352
6352
  }
6353
6353
  return [[i1_padded, c2, i2_padded]];
6354
6354
  };
6355
+ var generatePerimeterCornerTraceDetours = ({
6356
+ trace,
6357
+ chipBounds,
6358
+ clearance
6359
+ }) => {
6360
+ if (trace.tracePath.length < 4 || chipBounds.length === 0) return [];
6361
+ const start = trace.tracePath[0];
6362
+ const firstEscape = trace.tracePath[1];
6363
+ const lastEscape = trace.tracePath.at(-2);
6364
+ const end = trace.tracePath.at(-1);
6365
+ const minX = Math.min(...chipBounds.map((bounds) => bounds.minX)) - clearance;
6366
+ const maxX = Math.max(...chipBounds.map((bounds) => bounds.maxX)) + clearance;
6367
+ const minY = Math.min(...chipBounds.map((bounds) => bounds.minY)) - clearance;
6368
+ const maxY = Math.max(...chipBounds.map((bounds) => bounds.maxY)) + clearance;
6369
+ const candidates = [];
6370
+ for (const channelX of [minX, maxX]) {
6371
+ for (const channelY of [minY, maxY]) {
6372
+ candidates.push({
6373
+ traceId: trace.mspPairId,
6374
+ path: simplifyPath([
6375
+ start,
6376
+ firstEscape,
6377
+ { x: firstEscape.x, y: channelY },
6378
+ { x: channelX, y: channelY },
6379
+ { x: channelX, y: lastEscape.y },
6380
+ lastEscape,
6381
+ end
6382
+ ])
6383
+ });
6384
+ candidates.push({
6385
+ traceId: trace.mspPairId,
6386
+ path: simplifyPath([
6387
+ start,
6388
+ firstEscape,
6389
+ { x: channelX, y: firstEscape.y },
6390
+ { x: channelX, y: channelY },
6391
+ { x: lastEscape.x, y: channelY },
6392
+ lastEscape,
6393
+ end
6394
+ ])
6395
+ });
6396
+ }
6397
+ }
6398
+ return candidates;
6399
+ };
6355
6400
  var generatePerpendicularTraceDetours = ({
6356
6401
  trace,
6357
6402
  segmentIndex,
@@ -6663,7 +6708,8 @@ var UntangleTraceSubsolver = class extends BaseSolver {
6663
6708
  const otherTrace = traces[secondIndex];
6664
6709
  if (trace.globalConnNetId === otherTrace.globalConnNetId) continue;
6665
6710
  const isInitialBundleCrossing = this._isTraceBundle(trace, otherTrace);
6666
- if (!isInitialBundleCrossing && !this.reroutedTraceIds.has(trace.mspPairId) && !this.reroutedTraceIds.has(otherTrace.mspPairId)) {
6711
+ const isEligibleInitialCrossing = this.input.eligibleTraceIds?.has(trace.mspPairId) === true || this.input.eligibleTraceIds?.has(otherTrace.mspPairId) === true;
6712
+ if (!isInitialBundleCrossing && !isEligibleInitialCrossing && !this.reroutedTraceIds.has(trace.mspPairId) && !this.reroutedTraceIds.has(otherTrace.mspPairId)) {
6667
6713
  continue;
6668
6714
  }
6669
6715
  const crossings = findPerpendicularPathCrossings(
@@ -6706,6 +6752,16 @@ var UntangleTraceSubsolver = class extends BaseSolver {
6706
6752
  obstacleEnd: crossing.trace.tracePath[crossing.segmentIndex + 1],
6707
6753
  chipBounds,
6708
6754
  clearance: this.input.paddingBuffer
6755
+ }),
6756
+ ...generatePerimeterCornerTraceDetours({
6757
+ trace: crossing.trace,
6758
+ chipBounds,
6759
+ clearance: this.input.paddingBuffer
6760
+ }),
6761
+ ...generatePerimeterCornerTraceDetours({
6762
+ trace: crossing.otherTrace,
6763
+ chipBounds,
6764
+ clearance: this.input.paddingBuffer
6709
6765
  })
6710
6766
  ];
6711
6767
  const chipObstacles = getObstacleRects(this.input.inputProblem).filter(
@@ -12622,47 +12678,6 @@ var TraceElbowTransitionSimplificationSolver = class extends BaseSolver {
12622
12678
  }
12623
12679
  };
12624
12680
 
12625
- // lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts
12626
- var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
12627
- const segments = [];
12628
- let runStart = null;
12629
- let runAxis = null;
12630
- let runSign = 0;
12631
- const closeRun = (runEnd) => {
12632
- if (runStart && runAxis) {
12633
- const length = runAxis === "x" ? Math.abs(runEnd.x - runStart.x) : Math.abs(runEnd.y - runStart.y);
12634
- if (length > epsilon) {
12635
- segments.push({ start: runStart, end: runEnd, axis: runAxis, length });
12636
- }
12637
- }
12638
- runStart = null;
12639
- runAxis = null;
12640
- runSign = 0;
12641
- };
12642
- for (let i = 0; i < path.length - 1; i++) {
12643
- const a = path[i];
12644
- const b = path[i + 1];
12645
- const dx = b.x - a.x;
12646
- const dy = b.y - a.y;
12647
- if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue;
12648
- const axis = Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null;
12649
- if (!axis) {
12650
- closeRun(a);
12651
- continue;
12652
- }
12653
- const sign = Math.sign(axis === "x" ? dx : dy);
12654
- if (runStart && runAxis === axis && runSign === sign) continue;
12655
- closeRun(a);
12656
- runStart = a;
12657
- runAxis = axis;
12658
- runSign = sign;
12659
- }
12660
- if (path.length >= 2) {
12661
- closeRun(path[path.length - 1]);
12662
- }
12663
- return segments.sort((a, b) => b.length - a.length);
12664
- };
12665
-
12666
12681
  // lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts
12667
12682
  var getStubDirection = (path) => {
12668
12683
  const [start, end] = path;
@@ -12811,6 +12826,47 @@ var alignPortOnlyInlineNetLabelStubs = ({
12811
12826
  return alignedPlacements;
12812
12827
  };
12813
12828
 
12829
+ // lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts
12830
+ var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
12831
+ const segments = [];
12832
+ let runStart = null;
12833
+ let runAxis = null;
12834
+ let runSign = 0;
12835
+ const closeRun = (runEnd) => {
12836
+ if (runStart && runAxis) {
12837
+ const length = runAxis === "x" ? Math.abs(runEnd.x - runStart.x) : Math.abs(runEnd.y - runStart.y);
12838
+ if (length > epsilon) {
12839
+ segments.push({ start: runStart, end: runEnd, axis: runAxis, length });
12840
+ }
12841
+ }
12842
+ runStart = null;
12843
+ runAxis = null;
12844
+ runSign = 0;
12845
+ };
12846
+ for (let i = 0; i < path.length - 1; i++) {
12847
+ const a = path[i];
12848
+ const b = path[i + 1];
12849
+ const dx = b.x - a.x;
12850
+ const dy = b.y - a.y;
12851
+ if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue;
12852
+ const axis = Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null;
12853
+ if (!axis) {
12854
+ closeRun(a);
12855
+ continue;
12856
+ }
12857
+ const sign = Math.sign(axis === "x" ? dx : dy);
12858
+ if (runStart && runAxis === axis && runSign === sign) continue;
12859
+ closeRun(a);
12860
+ runStart = a;
12861
+ runAxis = axis;
12862
+ runSign = sign;
12863
+ }
12864
+ if (path.length >= 2) {
12865
+ closeRun(path[path.length - 1]);
12866
+ }
12867
+ return segments.sort((a, b) => b.length - a.length);
12868
+ };
12869
+
12814
12870
  // lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts
12815
12871
  var LABEL_CLEARANCE2 = 0.05;
12816
12872
  var POINT_EPSILON = 1e-6;
@@ -13173,6 +13229,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
13173
13229
  // lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts
13174
13230
  var DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
13175
13231
  var INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
13232
+ var INLINE_NET_LABEL_STUB_TRACE_CLEARANCE = 0.05;
13176
13233
  var INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
13177
13234
  var getPinPairKey2 = (pinIds) => [...pinIds].sort().join("::");
13178
13235
  var InlineNetLabelSolver = class extends BaseSolver {
@@ -13273,7 +13330,15 @@ var InlineNetLabelSolver = class extends BaseSolver {
13273
13330
  const stubLength = Math.max(width + 0.2, 0.6);
13274
13331
  const start = inputPin ? { x: inputPin.x, y: inputPin.y } : anchoredPlacement.anchorPoint;
13275
13332
  const direction = inputPin && inputChip ? inputPin._facingDirection ?? getPinDirection(inputPin, inputChip) : anchoredPlacement.orientation;
13276
- const end = direction === "x+" ? { x: start.x + stubLength, y: start.y } : direction === "x-" ? { x: start.x - stubLength, y: start.y } : direction === "y+" ? { x: start.x, y: start.y + stubLength } : { x: start.x, y: start.y - stubLength };
13333
+ const intendedEnd = direction === "x+" ? { x: start.x + stubLength, y: start.y } : direction === "x-" ? { x: start.x - stubLength, y: start.y } : direction === "y+" ? { x: start.x, y: start.y + stubLength } : { x: start.x, y: start.y - stubLength };
13334
+ const end = getCollisionLimitedTerminalStubEnd({
13335
+ start,
13336
+ intendedEnd,
13337
+ minimumLength: width + 2 * INLINE_NET_LABEL_TRACE_MARGIN,
13338
+ traces: this.traces,
13339
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId
13340
+ });
13341
+ if (!end) return null;
13277
13342
  const axis = direction === "x+" || direction === "x-" ? "x" : "y";
13278
13343
  const side = axis === "x" ? "y+" : "x-";
13279
13344
  const anchorPoint = {
@@ -13625,6 +13690,55 @@ var doesPathIntersectBounds2 = (path, bounds) => {
13625
13690
  }
13626
13691
  return false;
13627
13692
  };
13693
+ var GEOMETRY_EPSILON = 1e-6;
13694
+ var getCollisionLimitedTerminalStubEnd = ({
13695
+ start,
13696
+ intendedEnd,
13697
+ minimumLength,
13698
+ traces,
13699
+ ownGlobalConnNetId
13700
+ }) => {
13701
+ const isHorizontal4 = Math.abs(intendedEnd.x - start.x) >= Math.abs(intendedEnd.y - start.y);
13702
+ const alongAxis = isHorizontal4 ? "x" : "y";
13703
+ const acrossAxis = isHorizontal4 ? "y" : "x";
13704
+ const direction = Math.sign(intendedEnd[alongAxis] - start[alongAxis]) || 1;
13705
+ const intendedLength = Math.abs(intendedEnd[alongAxis] - start[alongAxis]);
13706
+ let availableLength = intendedLength;
13707
+ for (const trace of traces) {
13708
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue;
13709
+ for (let segmentIndex = 0; segmentIndex < trace.tracePath.length - 1; segmentIndex++) {
13710
+ const segmentStart = trace.tracePath[segmentIndex];
13711
+ const segmentEnd = trace.tracePath[segmentIndex + 1];
13712
+ const startAlong = (segmentStart[alongAxis] - start[alongAxis]) * direction;
13713
+ const endAlong = (segmentEnd[alongAxis] - start[alongAxis]) * direction;
13714
+ const startAcross = segmentStart[acrossAxis] - start[acrossAxis];
13715
+ const endAcross = segmentEnd[acrossAxis] - start[acrossAxis];
13716
+ const minAlong = Math.min(startAlong, endAlong);
13717
+ const maxAlong = Math.max(startAlong, endAlong);
13718
+ const minAcross = Math.min(startAcross, endAcross);
13719
+ const maxAcross = Math.max(startAcross, endAcross);
13720
+ let collisionDistance;
13721
+ const isCollinear = Math.abs(startAcross) <= GEOMETRY_EPSILON && Math.abs(endAcross) <= GEOMETRY_EPSILON;
13722
+ if (isCollinear && maxAlong >= -GEOMETRY_EPSILON && minAlong <= intendedLength + GEOMETRY_EPSILON) {
13723
+ collisionDistance = Math.max(0, minAlong);
13724
+ } else {
13725
+ const crossesStubAxis = minAcross <= GEOMETRY_EPSILON && maxAcross >= -GEOMETRY_EPSILON;
13726
+ const perpendicularAlong = (startAlong + endAlong) / 2;
13727
+ if (crossesStubAxis && Math.abs(startAlong - endAlong) <= GEOMETRY_EPSILON && perpendicularAlong >= -GEOMETRY_EPSILON && perpendicularAlong <= intendedLength + GEOMETRY_EPSILON) {
13728
+ collisionDistance = Math.max(0, perpendicularAlong);
13729
+ }
13730
+ }
13731
+ if (collisionDistance !== void 0) {
13732
+ availableLength = Math.min(
13733
+ availableLength,
13734
+ collisionDistance - INLINE_NET_LABEL_STUB_TRACE_CLEARANCE
13735
+ );
13736
+ }
13737
+ }
13738
+ }
13739
+ if (availableLength + GEOMETRY_EPSILON < minimumLength) return null;
13740
+ return isHorizontal4 ? { x: start.x + direction * availableLength, y: start.y } : { x: start.x, y: start.y + direction * availableLength };
13741
+ };
13628
13742
 
13629
13743
  // lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts
13630
13744
  function definePipelineStep(solverName, solverClass, getConstructorParams, opts = {}) {
@@ -13803,6 +13917,42 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
13803
13917
  definePipelineStep("traceCleanupSolver", TraceCleanupSolver, (instance) => {
13804
13918
  const prevSolverOutput = instance.traceElbowTransitionSimplificationSolver.getOutput();
13805
13919
  const traces = prevSolverOutput.traces;
13920
+ const overlapShiftSolver = instance.traceOverlapShiftSolver;
13921
+ const inlineLabelNetIds = new Set(
13922
+ [
13923
+ ...instance.inputProblem.directConnections,
13924
+ ...instance.inputProblem.netConnections
13925
+ ].filter((connection) => connection.allowInlineNetLabel).map((connection) => connection.netId)
13926
+ );
13927
+ const traceIdsInNewCrossingsAfterOverlapShift = /* @__PURE__ */ new Set();
13928
+ for (let traceIndex = 0; traceIndex < overlapShiftSolver.inputTracePaths.length; traceIndex++) {
13929
+ const inputTrace = overlapShiftSolver.inputTracePaths[traceIndex];
13930
+ const shiftedTrace = overlapShiftSolver.correctedTraceMap[inputTrace.mspPairId];
13931
+ for (let otherTraceIndex = traceIndex + 1; otherTraceIndex < overlapShiftSolver.inputTracePaths.length; otherTraceIndex++) {
13932
+ const otherInputTrace = overlapShiftSolver.inputTracePaths[otherTraceIndex];
13933
+ if (inputTrace.globalConnNetId === otherInputTrace.globalConnNetId) {
13934
+ continue;
13935
+ }
13936
+ if (!inlineLabelNetIds.has(inputTrace.userNetId ?? "") && !inlineLabelNetIds.has(otherInputTrace.userNetId ?? "")) {
13937
+ continue;
13938
+ }
13939
+ const otherShiftedTrace = overlapShiftSolver.correctedTraceMap[otherInputTrace.mspPairId];
13940
+ const crossingCountBeforeShift = findPerpendicularPathCrossings(
13941
+ inputTrace.tracePath,
13942
+ otherInputTrace.tracePath
13943
+ ).length;
13944
+ const crossingCountAfterShift = findPerpendicularPathCrossings(
13945
+ shiftedTrace.tracePath,
13946
+ otherShiftedTrace.tracePath
13947
+ ).length;
13948
+ if (crossingCountAfterShift > crossingCountBeforeShift) {
13949
+ traceIdsInNewCrossingsAfterOverlapShift.add(inputTrace.mspPairId);
13950
+ traceIdsInNewCrossingsAfterOverlapShift.add(
13951
+ otherInputTrace.mspPairId
13952
+ );
13953
+ }
13954
+ }
13955
+ }
13806
13956
  const labelMergingOutput = instance.traceLabelOverlapAvoidanceSolver.labelMergingSolver.getOutput();
13807
13957
  return [
13808
13958
  {
@@ -13810,7 +13960,8 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
13810
13960
  allTraces: traces,
13811
13961
  allLabelPlacements: labelMergingOutput.netLabelPlacements,
13812
13962
  mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
13813
- paddingBuffer: 0.1
13963
+ paddingBuffer: 0.1,
13964
+ eligibleTraceIds: traceIdsInNewCrossingsAfterOverlapShift
13814
13965
  }
13815
13966
  ];
13816
13967
  }),
@@ -14153,6 +14304,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
14153
14304
  export {
14154
14305
  DEFAULT_INLINE_NET_LABEL_HEIGHT,
14155
14306
  INLINE_NET_LABEL_MAX_SPAN_JOG,
14307
+ INLINE_NET_LABEL_STUB_TRACE_CLEARANCE,
14156
14308
  INLINE_NET_LABEL_TRACE_MARGIN,
14157
14309
  InlineNetLabelSolver,
14158
14310
  SchematicTracePipelineSolver,
@@ -13,11 +13,11 @@ import type {
13
13
  } from "lib/types/InputProblem"
14
14
  import { getColorFromString } from "lib/utils/getColorFromString"
15
15
  import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
16
+ import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
16
17
  import {
17
18
  type AxisAlignedSegment,
18
19
  getAxisAlignedSegments,
19
20
  } from "./getAxisAlignedSegments"
20
- import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
21
21
  import { pushAnchoredNetLabelsAwayFromInlineLabels } from "./pushAnchoredNetLabelsAwayFromInlineLabels"
22
22
 
23
23
  export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
@@ -27,6 +27,9 @@ export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
27
27
  */
28
28
  export const INLINE_NET_LABEL_TRACE_MARGIN = 0.05
29
29
 
30
+ /** Clearance between a generated terminal stub and an unrelated trace. */
31
+ export const INLINE_NET_LABEL_STUB_TRACE_CLEARANCE = 0.05
32
+
30
33
  /**
31
34
  * Largest perpendicular deviation a route may have and still be labeled as one
32
35
  * span. A route that is straight except for elbow jogs up to this size reads
@@ -248,7 +251,7 @@ export class InlineNetLabelSolver extends BaseSolver {
248
251
  inputPin && inputChip
249
252
  ? (inputPin._facingDirection ?? getPinDirection(inputPin, inputChip))
250
253
  : anchoredPlacement.orientation
251
- const end: Point =
254
+ const intendedEnd: Point =
252
255
  direction === "x+"
253
256
  ? { x: start.x + stubLength, y: start.y }
254
257
  : direction === "x-"
@@ -257,6 +260,15 @@ export class InlineNetLabelSolver extends BaseSolver {
257
260
  ? { x: start.x, y: start.y + stubLength }
258
261
  : { x: start.x, y: start.y - stubLength }
259
262
 
263
+ const end = getCollisionLimitedTerminalStubEnd({
264
+ start,
265
+ intendedEnd,
266
+ minimumLength: width + 2 * INLINE_NET_LABEL_TRACE_MARGIN,
267
+ traces: this.traces,
268
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId,
269
+ })
270
+ if (!end) return null
271
+
260
272
  const axis: InlineNetLabelPlacement["axis"] =
261
273
  direction === "x+" || direction === "x-" ? "x" : "y"
262
274
  const side: InlineNetLabelPlacement["side"] = axis === "x" ? "y+" : "x-"
@@ -777,3 +789,90 @@ const doesPathIntersectBounds = (path: Point[], bounds: Bounds): boolean => {
777
789
  }
778
790
  return false
779
791
  }
792
+
793
+ const GEOMETRY_EPSILON = 1e-6
794
+
795
+ /**
796
+ * Shortens an axis-aligned terminal stub before the first unrelated trace.
797
+ * Coordinates are schematic-world millimetres with +x right and +y up.
798
+ */
799
+ const getCollisionLimitedTerminalStubEnd = ({
800
+ start,
801
+ intendedEnd,
802
+ minimumLength,
803
+ traces,
804
+ ownGlobalConnNetId,
805
+ }: {
806
+ start: Point
807
+ intendedEnd: Point
808
+ minimumLength: number
809
+ traces: SolvedTracePath[]
810
+ ownGlobalConnNetId: string
811
+ }): Point | null => {
812
+ const isHorizontal =
813
+ Math.abs(intendedEnd.x - start.x) >= Math.abs(intendedEnd.y - start.y)
814
+ const alongAxis = isHorizontal ? "x" : "y"
815
+ const acrossAxis = isHorizontal ? "y" : "x"
816
+ const direction = Math.sign(intendedEnd[alongAxis] - start[alongAxis]) || 1
817
+ const intendedLength = Math.abs(intendedEnd[alongAxis] - start[alongAxis])
818
+ let availableLength = intendedLength
819
+
820
+ for (const trace of traces) {
821
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue
822
+
823
+ for (
824
+ let segmentIndex = 0;
825
+ segmentIndex < trace.tracePath.length - 1;
826
+ segmentIndex++
827
+ ) {
828
+ const segmentStart = trace.tracePath[segmentIndex]!
829
+ const segmentEnd = trace.tracePath[segmentIndex + 1]!
830
+ const startAlong =
831
+ (segmentStart[alongAxis] - start[alongAxis]) * direction
832
+ const endAlong = (segmentEnd[alongAxis] - start[alongAxis]) * direction
833
+ const startAcross = segmentStart[acrossAxis] - start[acrossAxis]
834
+ const endAcross = segmentEnd[acrossAxis] - start[acrossAxis]
835
+ const minAlong = Math.min(startAlong, endAlong)
836
+ const maxAlong = Math.max(startAlong, endAlong)
837
+ const minAcross = Math.min(startAcross, endAcross)
838
+ const maxAcross = Math.max(startAcross, endAcross)
839
+
840
+ let collisionDistance: number | undefined
841
+ const isCollinear =
842
+ Math.abs(startAcross) <= GEOMETRY_EPSILON &&
843
+ Math.abs(endAcross) <= GEOMETRY_EPSILON
844
+ if (
845
+ isCollinear &&
846
+ maxAlong >= -GEOMETRY_EPSILON &&
847
+ minAlong <= intendedLength + GEOMETRY_EPSILON
848
+ ) {
849
+ collisionDistance = Math.max(0, minAlong)
850
+ } else {
851
+ const crossesStubAxis =
852
+ minAcross <= GEOMETRY_EPSILON && maxAcross >= -GEOMETRY_EPSILON
853
+ const perpendicularAlong = (startAlong + endAlong) / 2
854
+ if (
855
+ crossesStubAxis &&
856
+ Math.abs(startAlong - endAlong) <= GEOMETRY_EPSILON &&
857
+ perpendicularAlong >= -GEOMETRY_EPSILON &&
858
+ perpendicularAlong <= intendedLength + GEOMETRY_EPSILON
859
+ ) {
860
+ collisionDistance = Math.max(0, perpendicularAlong)
861
+ }
862
+ }
863
+
864
+ if (collisionDistance !== undefined) {
865
+ availableLength = Math.min(
866
+ availableLength,
867
+ collisionDistance - INLINE_NET_LABEL_STUB_TRACE_CLEARANCE,
868
+ )
869
+ }
870
+ }
871
+ }
872
+
873
+ if (availableLength + GEOMETRY_EPSILON < minimumLength) return null
874
+
875
+ return isHorizontal
876
+ ? { x: start.x + direction * availableLength, y: start.y }
877
+ : { x: start.x, y: start.y + direction * availableLength }
878
+ }
@@ -35,6 +35,7 @@ import { UnroutedTraceRecoverySolver } from "../UnroutedTraceRecoverySolver/Unro
35
35
  import { SameNetJunctionAlignmentSolver } from "../SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver"
36
36
  import { TraceElbowTransitionSimplificationSolver } from "../TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver"
37
37
  import { InlineNetLabelSolver } from "../InlineNetLabelSolver/InlineNetLabelSolver"
38
+ import { findPerpendicularPathCrossings } from "../TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles"
38
39
 
39
40
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
40
41
  solverName: string
@@ -259,6 +260,61 @@ export class SchematicTracePipelineSolver extends BaseSolver {
259
260
  const prevSolverOutput =
260
261
  instance.traceElbowTransitionSimplificationSolver!.getOutput()
261
262
  const traces = prevSolverOutput.traces
263
+ const overlapShiftSolver = instance.traceOverlapShiftSolver!
264
+ const inlineLabelNetIds = new Set(
265
+ [
266
+ ...instance.inputProblem.directConnections,
267
+ ...instance.inputProblem.netConnections,
268
+ ]
269
+ .filter((connection) => connection.allowInlineNetLabel)
270
+ .map((connection) => connection.netId),
271
+ )
272
+ // An overlap shift can separate collinear inline-label traces by creating
273
+ // a new perpendicular crossing. Limit the extra untangle work to those
274
+ // newly introduced crossings so established routes remain stable.
275
+ const traceIdsInNewCrossingsAfterOverlapShift = new Set<string>()
276
+ for (
277
+ let traceIndex = 0;
278
+ traceIndex < overlapShiftSolver.inputTracePaths.length;
279
+ traceIndex++
280
+ ) {
281
+ const inputTrace = overlapShiftSolver.inputTracePaths[traceIndex]!
282
+ const shiftedTrace =
283
+ overlapShiftSolver.correctedTraceMap[inputTrace.mspPairId]!
284
+ for (
285
+ let otherTraceIndex = traceIndex + 1;
286
+ otherTraceIndex < overlapShiftSolver.inputTracePaths.length;
287
+ otherTraceIndex++
288
+ ) {
289
+ const otherInputTrace =
290
+ overlapShiftSolver.inputTracePaths[otherTraceIndex]!
291
+ if (inputTrace.globalConnNetId === otherInputTrace.globalConnNetId) {
292
+ continue
293
+ }
294
+ if (
295
+ !inlineLabelNetIds.has(inputTrace.userNetId ?? "") &&
296
+ !inlineLabelNetIds.has(otherInputTrace.userNetId ?? "")
297
+ ) {
298
+ continue
299
+ }
300
+ const otherShiftedTrace =
301
+ overlapShiftSolver.correctedTraceMap[otherInputTrace.mspPairId]!
302
+ const crossingCountBeforeShift = findPerpendicularPathCrossings(
303
+ inputTrace.tracePath,
304
+ otherInputTrace.tracePath,
305
+ ).length
306
+ const crossingCountAfterShift = findPerpendicularPathCrossings(
307
+ shiftedTrace.tracePath,
308
+ otherShiftedTrace.tracePath,
309
+ ).length
310
+ if (crossingCountAfterShift > crossingCountBeforeShift) {
311
+ traceIdsInNewCrossingsAfterOverlapShift.add(inputTrace.mspPairId)
312
+ traceIdsInNewCrossingsAfterOverlapShift.add(
313
+ otherInputTrace.mspPairId,
314
+ )
315
+ }
316
+ }
317
+ }
262
318
 
263
319
  const labelMergingOutput =
264
320
  instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput()
@@ -270,6 +326,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
270
326
  allLabelPlacements: labelMergingOutput.netLabelPlacements,
271
327
  mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
272
328
  paddingBuffer: 0.1,
329
+ eligibleTraceIds: traceIdsInNewCrossingsAfterOverlapShift,
273
330
  },
274
331
  ]
275
332
  }),
@@ -12,6 +12,7 @@ import {
12
12
  } from "./findIntersectionsWithObstacles"
13
13
  import {
14
14
  generateLShapeRerouteCandidates,
15
+ generatePerimeterCornerTraceDetours,
15
16
  generatePerpendicularTraceDetours,
16
17
  } from "./generateLShapeRerouteCandidates"
17
18
  import { isPathColliding, type CollisionInfo } from "./isPathColliding"
@@ -53,6 +54,7 @@ export interface UntangleTraceSubsolverInput {
53
54
  allLabelPlacements: NetLabelPlacement[]
54
55
  mergedLabelNetIdMap: Record<string, Set<string>>
55
56
  paddingBuffer: number
57
+ eligibleTraceIds?: ReadonlySet<string>
56
58
  }
57
59
 
58
60
  /**
@@ -127,7 +129,7 @@ export class UntangleTraceSubsolver extends BaseSolver {
127
129
 
128
130
  if (this.processingCrossings) {
129
131
  // The L-shape pass below only reacts when both arms intersect obstacles.
130
- // Resolve strict crossings on merged-label bundles before entering it.
132
+ // Resolve eligible strict cross-net crossings before entering it.
131
133
  const crossing = this._findCrossing()
132
134
  if (crossing) {
133
135
  if (!this._resolveCrossing(crossing)) {
@@ -207,8 +209,12 @@ export class UntangleTraceSubsolver extends BaseSolver {
207
209
  const otherTrace = traces[secondIndex]!
208
210
  if (trace.globalConnNetId === otherTrace.globalConnNetId) continue
209
211
  const isInitialBundleCrossing = this._isTraceBundle(trace, otherTrace)
212
+ const isEligibleInitialCrossing =
213
+ this.input.eligibleTraceIds?.has(trace.mspPairId) === true ||
214
+ this.input.eligibleTraceIds?.has(otherTrace.mspPairId) === true
210
215
  if (
211
216
  !isInitialBundleCrossing &&
217
+ !isEligibleInitialCrossing &&
212
218
  !this.reroutedTraceIds.has(trace.mspPairId) &&
213
219
  !this.reroutedTraceIds.has(otherTrace.mspPairId)
214
220
  ) {
@@ -259,6 +265,16 @@ export class UntangleTraceSubsolver extends BaseSolver {
259
265
  chipBounds,
260
266
  clearance: this.input.paddingBuffer,
261
267
  }),
268
+ ...generatePerimeterCornerTraceDetours({
269
+ trace: crossing.trace,
270
+ chipBounds,
271
+ clearance: this.input.paddingBuffer,
272
+ }),
273
+ ...generatePerimeterCornerTraceDetours({
274
+ trace: crossing.otherTrace,
275
+ chipBounds,
276
+ clearance: this.input.paddingBuffer,
277
+ }),
262
278
  ]
263
279
  const chipObstacles = getObstacleRects(this.input.inputProblem).filter(
264
280
  (obstacle) => obstacle.kind === "chip",
@@ -122,6 +122,58 @@ export interface TraceDetourCandidate {
122
122
  path: Point[]
123
123
  }
124
124
 
125
+ export const generatePerimeterCornerTraceDetours = ({
126
+ trace,
127
+ chipBounds,
128
+ clearance,
129
+ }: Omit<
130
+ PerpendicularTraceDetourInput,
131
+ "segmentIndex" | "obstacleStart" | "obstacleEnd"
132
+ >): TraceDetourCandidate[] => {
133
+ if (trace.tracePath.length < 4 || chipBounds.length === 0) return []
134
+
135
+ const start = trace.tracePath[0]!
136
+ const firstEscape = trace.tracePath[1]!
137
+ const lastEscape = trace.tracePath.at(-2)!
138
+ const end = trace.tracePath.at(-1)!
139
+ const minX = Math.min(...chipBounds.map((bounds) => bounds.minX)) - clearance
140
+ const maxX = Math.max(...chipBounds.map((bounds) => bounds.maxX)) + clearance
141
+ const minY = Math.min(...chipBounds.map((bounds) => bounds.minY)) - clearance
142
+ const maxY = Math.max(...chipBounds.map((bounds) => bounds.maxY)) + clearance
143
+ const candidates: TraceDetourCandidate[] = []
144
+
145
+ for (const channelX of [minX, maxX]) {
146
+ for (const channelY of [minY, maxY]) {
147
+ candidates.push({
148
+ traceId: trace.mspPairId,
149
+ path: simplifyPath([
150
+ start,
151
+ firstEscape,
152
+ { x: firstEscape.x, y: channelY },
153
+ { x: channelX, y: channelY },
154
+ { x: channelX, y: lastEscape.y },
155
+ lastEscape,
156
+ end,
157
+ ]),
158
+ })
159
+ candidates.push({
160
+ traceId: trace.mspPairId,
161
+ path: simplifyPath([
162
+ start,
163
+ firstEscape,
164
+ { x: channelX, y: firstEscape.y },
165
+ { x: channelX, y: channelY },
166
+ { x: lastEscape.x, y: channelY },
167
+ lastEscape,
168
+ end,
169
+ ]),
170
+ })
171
+ }
172
+ }
173
+
174
+ return candidates
175
+ }
176
+
125
177
  export const generatePerpendicularTraceDetours = ({
126
178
  trace,
127
179
  segmentIndex,
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.147",
8
+ "version": "0.0.149",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example51.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />