From d6b34330f3529c7dedf4020b3cb188adeaf915f1 Mon Sep 17 00:00:00 2001 From: Mustafa Mulla Date: Tue, 1 Sep 2026 13:18:30 +0530 Subject: [PATCH 01/10] fix: untangle generated net label connector crossings --- .../SchematicTracePipelineSolver.ts | 22 +- .../TraceCleanupSolver/TraceCleanupSolver.ts | 4 + .../sub-solver/UntangleTraceSubsolver.ts | 73 ++- .../findIntersectionsWithObstacles.ts | 26 +- .../generateLShapeRerouteCandidates.ts | 27 +- .../bug-report-20260706T213649Z.snap.svg | 4 +- .../bug-report-20260706T220324Z.snap.svg | 4 +- .../bug-report-20260707T092615Z.snap.svg | 4 +- .../bug-report-20260708T055430Z.snap.svg | 64 +-- .../bug-report-20260716T144856Z.snap.svg | 18 +- .../bug-report-20260717T022934Z.snap.svg | 466 +++++++++--------- .../bug-report-20260721T221026Z.snap.svg | 20 +- .../bug-report-20260728T225606Z.snap.svg | 4 +- .../bug-report-20260730T061837Z.snap.svg | 332 ++++++------- .../bug-report-20260804T171919Z.snap.svg | 4 +- .../bug-report-20260805T061316Z.snap.svg | 4 +- .../bug-report-20260806T061548Z.snap.svg | 4 +- .../bug-report-20260806T061548Z.test.ts | 14 +- .../bug-report-20260815T073240Z.snap.svg | 172 +++---- .../bug-report-20260819T091818Z.snap.svg | 36 +- .../bug-report-20260901T055358Z.snap.svg | 4 +- .../bug-report-20260901T064117Z.snap.svg | 4 +- .../examples/__snapshots__/example29.snap.svg | 4 +- .../examples/__snapshots__/example43.snap.svg | 4 +- .../examples/__snapshots__/example44.snap.svg | 20 +- .../examples/__snapshots__/example45.snap.svg | 4 +- .../board-1273-trace-overlap-cycle.snap.svg | 4 +- ...d1096-usb-label-overlap-iteration.snap.svg | 20 +- ...core-ground-inline-label-fallback.snap.svg | 4 +- .../repro-pmp11282-isolated-dcdc.snap.svg | 4 +- ...pro-rp2040-usb-cc2-ground-overlap.snap.svg | 4 +- ...rellis-core-gnd-net-label-overlap.snap.svg | 4 +- ...repro-usb-power-vbus-label-detour.snap.svg | 4 +- ...ro-wireless-mouse-charger-section.snap.svg | 4 +- ...pro-wireless-mouse-charger-section.test.ts | 20 + .../findPerpendicularPathCrossings.test.ts | 34 ++ 36 files changed, 806 insertions(+), 638 deletions(-) create mode 100644 tests/solvers/TraceCleanupSolver/findPerpendicularPathCrossings.test.ts diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 6a7591d21..2ee2800d9 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -463,6 +463,18 @@ export class SchematicTracePipelineSolver extends BaseSolver { instance.preAlignmentTraceElbowTransitionSimplificationSolver!.getOutput() const labelMergingOutput = instance.traceLabelOverlapAvoidanceSolver!.labelMergingSolver!.getOutput() + const initiallyRoutedTraceIds = new Set( + instance + .traceCleanupSolver!.getOutput() + .traces.map((trace) => trace.mspPairId), + ) + const generatedNetLabelConnectorTraceIds = new Set( + collisionOutput.traces + .filter((trace) => + trace.mspPairId.startsWith("available-net-orientation-"), + ) + .map((trace) => trace.mspPairId), + ) return [ { @@ -471,12 +483,10 @@ export class SchematicTracePipelineSolver extends BaseSolver { allLabelPlacements: collisionOutput.netLabelPlacements, mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap, paddingBuffer: 0.1, - operations: ["aligning_same_net_rails"], - eligibleTraceIds: new Set( - instance - .traceCleanupSolver!.getOutput() - .traces.map((trace) => trace.mspPairId), - ), + operations: ["untangling_traces", "aligning_same_net_rails"], + eligibleTraceIds: initiallyRoutedTraceIds, + untangleEligibleTraceIds: generatedNetLabelConnectorTraceIds, + includeTerminalSegmentCrossings: true, }, ] }, diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index da39fd852..a15292f5a 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -25,6 +25,8 @@ export interface TraceCleanupSolverInput { paddingBuffer: number operations?: readonly TraceCleanupOperation[] eligibleTraceIds?: ReadonlySet + untangleEligibleTraceIds?: ReadonlySet + includeTerminalSegmentCrossings?: boolean } import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" @@ -120,6 +122,8 @@ export class TraceCleanupSolver extends BaseSolver { this.activeSubSolver = new UntangleTraceSubsolver({ ...this.input, allTraces: Array.from(this.tracesMap.values()), + eligibleTraceIds: + this.input.untangleEligibleTraceIds ?? this.input.eligibleTraceIds, }) } diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts index 552e36f59..454b0fdf6 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts @@ -23,7 +23,7 @@ import { } from "./generateRectangleCandidates" import type { GraphicsObject } from "graphics-debug" -import type { Point } from "@tscircuit/math-utils" +import type { Bounds, Point } from "@tscircuit/math-utils" import { visualizeLSapes } from "./visualizeLSapes" import { visualizeIntersectionPoints } from "./visualizeIntersectionPoints" @@ -36,6 +36,9 @@ import { isPathCollidingWithChipInterior, } from "../../Example28Solver/geometry" import { getObstacleRects } from "../../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import { getRectBounds } from "../../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry" +import { tracePathContainsPoint } from "../../RailNetLabelCornerPlacementSolver/geometry" +import { hasCollisionsWithLabels } from "../hasCollisionsWithLabels" interface TraceCrossing { trace: SolvedTracePath @@ -55,6 +58,7 @@ export interface UntangleTraceSubsolverInput { mergedLabelNetIdMap: Record> paddingBuffer: number eligibleTraceIds?: ReadonlySet + includeTerminalSegmentCrossings?: boolean } /** @@ -224,6 +228,9 @@ export class UntangleTraceSubsolver extends BaseSolver { const crossings = findPerpendicularPathCrossings( trace.tracePath, otherTrace.tracePath, + { + includeTerminalSegments: this.input.includeTerminalSegmentCrossings, + }, ) for (const { pathSegmentIndex, otherPathSegmentIndex } of crossings) { const crossing = { @@ -242,6 +249,45 @@ export class UntangleTraceSubsolver extends BaseSolver { return null } + private _getTraceObstacleBounds( + trace: SolvedTracePath, + segmentIndex: number, + ): Bounds { + const segmentStart = trace.tracePath[segmentIndex]! + const segmentEnd = trace.tracePath[segmentIndex + 1]! + const bounds: Bounds = { + minX: Math.min(segmentStart.x, segmentEnd.x), + maxX: Math.max(segmentStart.x, segmentEnd.x), + minY: Math.min(segmentStart.y, segmentEnd.y), + maxY: Math.max(segmentStart.y, segmentEnd.y), + } + + for (const label of this.input.allLabelPlacements) { + if (label.globalConnNetId !== trace.globalConnNetId) continue + if (!label.pinIds.every((pinId) => trace.pinIds.includes(pinId))) continue + if (!tracePathContainsPoint(trace.tracePath, label.anchorPoint)) continue + + const labelBounds = getRectBounds(label.center, label.width, label.height) + bounds.minX = Math.min(bounds.minX, labelBounds.minX) + bounds.maxX = Math.max(bounds.maxX, labelBounds.maxX) + bounds.minY = Math.min(bounds.minY, labelBounds.minY) + bounds.maxY = Math.max(bounds.maxY, labelBounds.maxY) + } + + return bounds + } + + private _getForeignLabelBounds(trace: SolvedTracePath): Bounds[] { + return this.input.allLabelPlacements + .filter((label) => { + const mergedNetIds = + this.input.mergedLabelNetIdMap[label.globalConnNetId] + if (mergedNetIds) return !mergedNetIds.has(trace.globalConnNetId) + return label.globalConnNetId !== trace.globalConnNetId + }) + .map((label) => getRectBounds(label.center, label.width, label.height)) + } + private _resolveCrossing(crossing: TraceCrossing) { const chipBounds = this.chipObstacleSpatialIndex.chips.map( (chip) => chip.bounds, @@ -254,6 +300,10 @@ export class UntangleTraceSubsolver extends BaseSolver { crossing.otherTrace.tracePath[crossing.otherSegmentIndex]!, obstacleEnd: crossing.otherTrace.tracePath[crossing.otherSegmentIndex + 1]!, + obstacleBounds: this._getTraceObstacleBounds( + crossing.otherTrace, + crossing.otherSegmentIndex, + ), chipBounds, clearance: this.input.paddingBuffer, }), @@ -262,6 +312,10 @@ export class UntangleTraceSubsolver extends BaseSolver { segmentIndex: crossing.otherSegmentIndex, obstacleStart: crossing.trace.tracePath[crossing.segmentIndex]!, obstacleEnd: crossing.trace.tracePath[crossing.segmentIndex + 1]!, + obstacleBounds: this._getTraceObstacleBounds( + crossing.trace, + crossing.segmentIndex, + ), chipBounds, clearance: this.input.paddingBuffer, }), @@ -290,17 +344,24 @@ export class UntangleTraceSubsolver extends BaseSolver { candidate.traceId, ), })) - .filter( - (candidate) => + .filter((candidate) => { + const candidateTrace = this.input.allTraces.find( + (trace) => trace.mspPairId === candidate.traceId, + )! + return ( !isPathCollidingWithChipInterior(candidate.path, chipObstacles) && + !hasCollisionsWithLabels( + candidate.path, + this._getForeignLabelBounds(candidateTrace), + ) && !isPathColliding( candidate.path, [crossing.trace, crossing.otherTrace], candidate.traceId, ).isColliding && - (crossing.isInitialBundleCrossing || - !candidate.collision.isColliding), - ) + (crossing.isInitialBundleCrossing || !candidate.collision.isColliding) + ) + }) .sort( (first, second) => Number(first.collision.isColliding) - diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts b/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts index 863d07f99..46d4f7ced 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts @@ -9,6 +9,10 @@ export interface PerpendicularPathCrossing { otherPathSegmentIndex: number } +interface FindPerpendicularPathCrossingsOptions { + includeTerminalSegments?: boolean +} + /** * Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles. * It iterates through each segment of every obstacle and checks for intersections with the input segment. @@ -48,14 +52,24 @@ const isSamePoint = (first: Point, second: Point) => export const findPerpendicularPathCrossings = ( path: Point[], otherPath: Point[], + options: FindPerpendicularPathCrossingsOptions = {}, ): PerpendicularPathCrossing[] => { const crossings: PerpendicularPathCrossing[] = [] + const firstPathSegmentIndex = options.includeTerminalSegments ? 0 : 1 + const lastPathSegmentIndex = options.includeTerminalSegments + ? path.length - 1 + : path.length - 2 + const firstOtherPathSegmentIndex = options.includeTerminalSegments ? 0 : 1 + const lastOtherPathSegmentIndex = options.includeTerminalSegments + ? otherPath.length - 1 + : otherPath.length - 2 - // Terminal segments connect to pins and are allowed to meet other traces at - // their endpoints. Only internal, strict crossings need to be untangled. + // Terminal segments connect to pins and may meet other traces at their + // endpoints, but a strict crossing through the interior of a terminal + // segment can be included by post-routing cleanup stages. for ( - let pathSegmentIndex = 1; - pathSegmentIndex < path.length - 2; + let pathSegmentIndex = firstPathSegmentIndex; + pathSegmentIndex < lastPathSegmentIndex; pathSegmentIndex++ ) { const start = path[pathSegmentIndex]! @@ -63,8 +77,8 @@ export const findPerpendicularPathCrossings = ( const isVertical = Math.abs(start.x - end.x) < EPS for ( - let otherPathSegmentIndex = 1; - otherPathSegmentIndex < otherPath.length - 2; + let otherPathSegmentIndex = firstOtherPathSegmentIndex; + otherPathSegmentIndex < lastOtherPathSegmentIndex; otherPathSegmentIndex++ ) { const otherStart = otherPath[otherPathSegmentIndex]! diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts b/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts index 6fbf3ed49..39923f131 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts @@ -113,6 +113,7 @@ export interface PerpendicularTraceDetourInput { segmentIndex: number obstacleStart: Point obstacleEnd: Point + obstacleBounds?: Bounds chipBounds: Bounds[] clearance: number } @@ -179,6 +180,7 @@ export const generatePerpendicularTraceDetours = ({ segmentIndex, obstacleStart, obstacleEnd, + obstacleBounds, chipBounds, clearance, }: PerpendicularTraceDetourInput): TraceDetourCandidate[] => { @@ -187,18 +189,29 @@ export const generatePerpendicularTraceDetours = ({ const end = path[index + 1]! const movingAxis: "x" | "y" = Math.abs(start.x - end.x) < EPS ? "y" : "x" const detourAxis = movingAxis === "x" ? "y" : "x" + const bounds = obstacleBounds ?? { + minX: Math.min(obstacleStart.x, obstacleEnd.x), + maxX: Math.max(obstacleStart.x, obstacleEnd.x), + minY: Math.min(obstacleStart.y, obstacleEnd.y), + maxY: Math.max(obstacleStart.y, obstacleEnd.y), + } + const movingLowBound = movingAxis === "x" ? "minX" : "minY" + const movingHighBound = movingAxis === "x" ? "maxX" : "maxY" + const movingMidpoint = + (bounds[movingLowBound] + bounds[movingHighBound]) / 2 + const startSide = start[movingAxis] < movingMidpoint ? -1 : 1 const gate = - obstacleStart[movingAxis] + - Math.sign(start[movingAxis] - obstacleStart[movingAxis]) * clearance - const obstacleRange = [obstacleStart[detourAxis], obstacleEnd[detourAxis]] - const lowBound = detourAxis === "x" ? "minX" : "minY" - const highBound = detourAxis === "x" ? "maxX" : "maxY" + bounds[startSide < 0 ? movingLowBound : movingHighBound] + + startSide * clearance + const detourLowBound = detourAxis === "x" ? "minX" : "minY" + const detourHighBound = detourAxis === "x" ? "maxX" : "maxY" + const obstacleRange = [bounds[detourLowBound], bounds[detourHighBound]] const detourCoordinates = [ Math.min(...obstacleRange) - clearance, Math.max(...obstacleRange) + clearance, ...chipBounds.flatMap((bounds) => [ - bounds[lowBound] - clearance, - bounds[highBound] + clearance, + bounds[detourLowBound] - clearance, + bounds[detourHighBound] + clearance, ]), ] diff --git a/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg b/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg index e1eff49c6..3403d24ff 100644 --- a/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +++ b/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg @@ -1,6 +1,6 @@ - J112345678J212345678J3123456U112345678U212345678U312345678U412345678U512345678U612345678U712345678U812345678R1R2R3R4R5R6R7R8R9R10R11R12C1C2C3C4C5C6C7C8D1D2D3D4J112345678J212345678J3123456U112345678U212345678U312345678U412345678U512345678U612345678U712345678U812345678R1R2R3R4R5R6R7R8R9R10R11R12C1C2C3C4C5C6C7C8D1D2D3D4 - J81J11J61J71J21J31J51J41SLPWR11234SLPWR21234U31234567891011121314151617U21234567891011IC1123456789U112345678Q1123Q2123Q3123R1R2R3R4R5R6R7R8R9R10R11RS1RSR1C1C2C3C4C5C6C7C8C9C10C11Cin1Cout1C_VOLT1C_VOLT2L1L2D1D2D3D4D6D7J81J11J61J71J21J31J51J41SLPWR11234SLPWR21234U31234567891011121314151617U21234567891011IC1123456789U112345678Q1123Q2123Q3123R1R2R3R4R5R6R7R8R9R10R11RS1RSR1C1C2C3C4C5C6C7C8C9C10C11Cin1Cout1C_VOLT1C_VOLT2L1L2D1D2D3D4D6D7 - J_BAT_POS1234J_BAT_NEG1234D_PROTD_SENSE_CLAMPU_REF123U11234567891011121314U212345C1C2C_SENSERREFR_TOPR_BOTRTH_TOPRTH_100RTH_80RTH_60RTH_40RTH_20RHYS1RHYS2RHYS3RHYS4RHYS5RPU1RPU2RPU3RPU4RPU5LED1RLED1LED2RLED2LED3RLED3LED4RLED4LED5RLED5J_BAT_POS1234J_BAT_NEG1234D_PROTD_SENSE_CLAMPU_REF123U11234567891011121314U212345C1C2C_SENSERREFR_TOPR_BOTRTH_TOPRTH_100RTH_80RTH_60RTH_40RTH_20RHYS1RHYS2RHYS3RHYS4RHYS5RPU1RPU2RPU3RPU4RPU5LED1RLED1LED2RLED2LED3RLED3LED4RLED4LED5RLED5 - U_MCU1234567891011121314151617181920212223242526272829303132R_FAULT_PULLUPU_MCU1234567891011121314151617181920212223242526272829303132R_FAULT_PULLUPXXXXXXXXXXXXXXXXXXXXXXXXXXXATMEGA328P_AUU_MCU + " fill="rgba(255, 255, 255, 0.6)" stroke="rgb(132, 0, 0)" stroke-width="2.096710784956px"/>XXXXXATMEGA328P_AUU_MCU \ No newline at end of file diff --git a/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg b/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg index 33f359e34..fa1f91a8f 100644 --- a/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +++ b/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg @@ -1,12 +1,12 @@ - U112345678910C1C2C3C4R3R4R5D2JP1123JP212JP312JP61234JP71234JP412JP51234U112345678910C1C2C3C4R3R4R5D2JP1123JP212JP312JP61234JP71234JP412JP51234XXXXXX -