From a62e1ef48ed59727da2aae51970bf0ae78d1de4c Mon Sep 17 00:00:00 2001 From: hrithik18k Date: Sat, 12 Sep 2026 02:15:59 +0530 Subject: [PATCH] fix: anchor split net labels at component edge --- .../AvailableNetOrientationSolver.ts | 94 ++++++++++++++++++- .../AvailableNetOrientationSolver/types.ts | 1 + .../NetLabelToTraceSolver.ts | 46 ++++++++- .../bug-report-20260911T195723Z.snap.svg | 36 +++---- .../bug-report-20260911T195723Z.test.ts | 14 +++ .../examples/__snapshots__/example51.snap.svg | 20 ++-- ...al-net-label-branch-from-junction.snap.svg | 62 ++++++------ ...mal-net-label-branch-from-junction.test.ts | 11 +++ 8 files changed, 223 insertions(+), 61 deletions(-) diff --git a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts index baba205ed..3adab038c 100644 --- a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +++ b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts @@ -401,7 +401,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { ) if (tracePath.length < 2) return - const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}` + const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}${candidate.foldIntoHostTrace ? "-host-extension" : ""}` const connectorTrace: SolvedTracePath = { mspPairId, dcConnNetId: label.dcConnNetId ?? label.globalConnNetId, @@ -453,6 +453,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { (connection) => connection.netId === label.netId, ) const isTwoPinNet = netConnection?.pinIds.length === 2 + const isTwoPinLabel = label.pinIds.length === 2 const isDownwardGroundRail = netConnection?.isGround && isXOrientation(label.orientation) && @@ -510,6 +511,20 @@ export class AvailableNetOrientationSolver extends BaseSolver { if (traceAnchorCandidate) return traceAnchorCandidate } + if ( + isTwoPinLabel && + !this.isGroundLabel(label) && + orientations.length === 1 && + isYOrientation(requiredOrientation) + ) { + const endpointCandidate = this.findValidTwoPinEndpointCandidate( + label, + requiredOrientation, + labelIndex, + ) + if (endpointCandidate) return endpointCandidate + } + if ( isTwoPinNet && orientations.length === 1 && @@ -701,6 +716,83 @@ export class AvailableNetOrientationSolver extends BaseSolver { return null } + /** + * When a two-pin label was initially attached to the first bend beside a + * component, keep a vertical replacement on that component's pin column. + * This avoids turning the old bend into a tee when the pin itself provides + * the shorter, cleaner branch source. + */ + private findValidTwoPinEndpointCandidate( + label: NetLabelPlacement, + orientation: "y+" | "y-", + labelIndex: number, + ) { + const pins = label.pinIds.flatMap((pinId) => { + const pin = this.pinMap[pinId] + return pin ? [pin] : [] + }) + if ( + pins.length !== 2 || + pins[0]!._facingDirection !== pins[1]!._facingDirection || + (pins[0]!._facingDirection !== "x+" && pins[0]!._facingDirection !== "x-") + ) { + return null + } + + const endpointPins = label.mspConnectionPairIds.flatMap((traceId) => { + const trace = this.traceMap[traceId] + if (!trace || trace.tracePath.length < 2) return [] + + return [ + { point: trace.tracePath[0]!, neighbor: trace.tracePath[1]! }, + { + point: trace.tracePath.at(-1)!, + neighbor: trace.tracePath.at(-2)!, + }, + ].filter( + ({ point, neighbor }) => + Math.abs(neighbor.x - label.anchorPoint.x) <= EPS && + Math.abs(neighbor.y - label.anchorPoint.y) <= EPS && + (Math.abs(point.x - label.anchorPoint.x) > EPS || + Math.abs(point.y - label.anchorPoint.y) > EPS) && + trace.pins.some( + (pin) => + Math.abs(pin.x - point.x) <= EPS && + Math.abs(pin.y - point.y) <= EPS, + ), + ) + }) + + endpointPins.sort( + (first, second) => + Math.abs(first.point.x - label.anchorPoint.x) + + Math.abs(first.point.y - label.anchorPoint.y) - + (Math.abs(second.point.x - label.anchorPoint.x) + + Math.abs(second.point.y - label.anchorPoint.y)), + ) + + if (endpointPins.length > 0) { + const candidate = this.findValidCandidateInShiftColumn({ + label, + labelIndex, + orientation, + direction: dir(orientation), + baseAnchor: this.getWickOffsetAnchor(label.anchorPoint, orientation), + maxSearchDistance: this.maxSearchDistance, + outwardDistance: 0, + phase: "lateral-shift", + stopOnTraceCollision: false, + connectorSource: label.anchorPoint, + }) + if (candidate) { + candidate.foldIntoHostTrace = true + return candidate + } + } + + return null + } + private hasTraceContinuingInOrientation( label: NetLabelPlacement, orientation: "y+" | "y-", diff --git a/lib/solvers/AvailableNetOrientationSolver/types.ts b/lib/solvers/AvailableNetOrientationSolver/types.ts index 955089d0d..07efe91a4 100644 --- a/lib/solvers/AvailableNetOrientationSolver/types.ts +++ b/lib/solvers/AvailableNetOrientationSolver/types.ts @@ -26,6 +26,7 @@ export type CandidateLabel = { center: Point width: number height: number + foldIntoHostTrace?: boolean } export type CandidateStatus = diff --git a/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts b/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts index a894bcadb..c3be02e56 100644 --- a/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +++ b/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts @@ -27,6 +27,7 @@ import { type InlineNetLabelPlacement, visualizeInlineNetLabelOutput, } from "../InlineNetLabelSolver/InlineNetLabelSolver" +import { extendVerticalTracePathAtInteriorPoint } from "../AvailableNetOrientationSolver/traces" import { reduceTraceCrossings } from "./reduceTraceCrossings" type GlobalConnNetId = NetLabelPlacement["globalConnNetId"] @@ -52,6 +53,49 @@ const MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25 const getCanonicalPairKey = (firstPinId: PinId, secondPinId: PinId) => [firstPinId, secondPinId].sort().join("--") +const foldVerticalLabelConnectorsIntoHostTraces = ( + traces: SolvedTracePath[], +) => { + const removedTraceIds = new Set() + const output = [...traces] + + for (const connector of output) { + if (!connector.mspPairId.endsWith("-host-extension")) continue + const [sourcePoint, extensionEndPoint] = connector.tracePath + if ( + connector.tracePath.length !== 2 || + !sourcePoint || + !extensionEndPoint || + Math.abs(sourcePoint.x - extensionEndPoint.x) > 1e-6 + ) { + continue + } + + const hostTraceIndex = output.findIndex( + (trace) => + trace !== connector && + trace.globalConnNetId === connector.globalConnNetId && + extendVerticalTracePathAtInteriorPoint({ + tracePath: trace.tracePath, + sourcePoint, + extensionEndPoint, + }) !== null, + ) + if (hostTraceIndex === -1) continue + + const hostTrace = output[hostTraceIndex]! + const extendedTracePath = extendVerticalTracePathAtInteriorPoint({ + tracePath: hostTrace.tracePath, + sourcePoint, + extensionEndPoint, + })! + output[hostTraceIndex] = { ...hostTrace, tracePath: extendedTracePath } + removedTraceIds.add(connector.mspPairId) + } + + return output.filter((trace) => !removedTraceIds.has(trace.mspPairId)) +} + const getPerpendicularOffset = (firstPoint: Point, secondPoint: Point) => { const xDistance = Math.abs(firstPoint.x - secondPoint.x) const yDistance = Math.abs(firstPoint.y - secondPoint.y) @@ -93,7 +137,7 @@ export class NetLabelToTraceSolver extends BaseSolver { constructor(private input: InlineNetLabelOutput) { super() this.inputProblem = input.inputProblem - this.outputTraces = [...input.traces] + this.outputTraces = foldVerticalLabelConnectorsIntoHostTraces(input.traces) this.outputNetLabelPlacements = [...input.netLabelPlacements] const { chipMap, pinMap } = getTraceRecoveryConnectivityMaps( diff --git a/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg b/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg index 673e56802..098f11404 100644 --- a/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg +++ b/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg @@ -1,6 +1,6 @@ - EH1EH2pin13_alt1pin14_alt1A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12anodecathodeanodecathodeSTATVSSVBATVDDPROGanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1212VINGNDENNCVOUTanodecathodeanodecathodeGND13V3ENSENSOR_VPSENSOR_VNIO34IO35IO32IO33IO25IO26IO27IO14IO12GND2IO13NC1NC2NC3NC4NC5NC6IO15IO2IO0IO4IO16IO17IO5IO18IO19NC7IO21RXD0TXD0IO22IO23GND3GND4anodecathodeanodecathodeanodecathodeanodecathodeanodecathode12123V3GNDTXRXENIO0RESV_VDDIONC1NC2NC3NC4NC5AUX_CLVDDIOAD0REGOUTFSYNCINTVDDNC6NC7NC8NC9GNDRESV_NCRESV_GNDAUX_DANCSSCLSDAEPanodecathodeanodecathodeanodecathodeanodecathodeanodecathode123XXEH1EH2pin13_alt1pin14_alt1A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12anodecathodeanodecathodeSTATVSSVBATVDDPROGanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1212VINGNDENNCVOUTanodecathodeanodecathodeGND13V3ENSENSOR_VPSENSOR_VNIO34IO35IO32IO33IO25IO26IO27IO14IO12GND2IO13NC1NC2NC3NC4NC5NC6IO15IO2IO0IO4IO16IO17IO5IO18IO19NC7IO21RXD0TXD0IO22IO23GND3GND4anodecathodeanodecathodeanodecathodeanodecathodeanodecathode12123V3GNDTXRXENIO0RESV_VDDIONC1NC2NC3NC4NC5AUX_CLVDDIOAD0REGOUTFSYNCINTVDDNC6NC7NC8NC9GNDRESV_NCRESV_GNDAUX_DANCSSCLSDAEPanodecathodeanodecathodeanodecathodeanodecathodeanodecathode123XXXXXXXXXXXXXX { solver.solve() + const v3v3Label = solver.netLabelToTraceSolver!.outputNetLabelPlacements.find( + (label) => + label.netId === "V3V3" && label.pinIds.includes("schematic_port_141"), + ) + const v3v3Connector = solver.netLabelToTraceSolver!.outputTraces.find( + (trace) => trace.mspPairId === "available-net-orientation-43-V3V3", + ) + + expect(v3v3Label?.orientation).toBe("y+") + expect(v3v3Connector).toBeUndefined() + const v3v3HostTrace = solver.netLabelToTraceSolver!.outputTraces.find( + (trace) => trace.pinIds.includes("schematic_port_141"), + ) + expect(v3v3HostTrace?.tracePath).toContainEqual({ x: -6.5, y: -19 }) expect(solver).toMatchSolverSnapshot(import.meta.path) }) diff --git a/tests/examples/__snapshots__/example51.snap.svg b/tests/examples/__snapshots__/example51.snap.svg index 9c2de65d8..80620a5d8 100644 --- a/tests/examples/__snapshots__/example51.snap.svg +++ b/tests/examples/__snapshots__/example51.snap.svg @@ -1,6 +1,6 @@ - XXXXXX -