From ea7120d434aebf1f473d4036be28381651264176 Mon Sep 17 00:00:00 2001 From: techmannih Date: Thu, 10 Sep 2026 11:19:38 +0530 Subject: [PATCH 1/3] prevent trace detours from crossing same-net labels --- .../rerouteCollidingTrace.ts | 3 ++ .../SingleOverlapSolver.ts | 43 ++++++++++++++++++- .../trySnipAndReconnect.ts | 23 ++++++++++ .../bug-report-20260717T022934Z.snap.svg | 24 +++++------ .../bug-report-20260721T221026Z.snap.svg | 20 ++++----- .../bug-report-20260905T041712Z.snap.svg | 32 +++++++------- ...oost-drv8711-ground-label-overlap.snap.svg | 4 +- ...boost-drv8711-ground-label-overlap.test.ts | 15 +++++-- 8 files changed, 119 insertions(+), 45 deletions(-) diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts index e4c8bffa8..6b6ebc225 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts @@ -25,12 +25,14 @@ export const generateRerouteCandidates = ({ label, paddingBuffer, detourCount, + includeCornerDetours = false, }: { trace: SolvedTracePath label: NetLabelPlacement problem: InputProblem paddingBuffer: number detourCount: number + includeCornerDetours?: boolean }): Point[][] => { const initialTrace = { ...trace, tracePath: simplifyPath(trace.tracePath) } @@ -73,6 +75,7 @@ export const generateRerouteCandidates = ({ initialTrace, firstInsideIndex, lastInsideIndex, + includeCornerDetours, labelBounds, paddingBuffer, detourCount, diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts index ff48d07d7..3d70baf71 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts @@ -11,6 +11,8 @@ import { generateRerouteCandidates } from "../../rerouteCollidingTrace" import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap" import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces" +import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry" +import { pathEntersAnyNetLabel } from "lib/solvers/SameNetJunctionAlignmentSolver/pathIntersectsAnyNetLabel" interface SingleOverlapSolverInput { trace: SolvedTracePath @@ -64,6 +66,21 @@ export class SingleOverlapSolver extends BaseSolver { this.netLabelPlacements = solverInput.netLabelPlacements ?? [ solverInput.label, ] + // Attached labels move with this trace. Protect the interiors of other + // same-net labels that the original route already clears. + const sameNetLabelsToAvoid = this.netLabelPlacements.filter( + (label) => + label.globalConnNetId === this.initialTrace.globalConnNetId && + !label.mspConnectionPairIds.includes(this.initialTrace.mspPairId) && + !tracePathContainsPoint( + this.initialTrace.tracePath, + label.anchorPoint, + ) && + !pathEntersAnyNetLabel({ + path: this.initialTrace.tracePath, + netLabelPlacements: [label], + }), + ) this.obstacles = getObstacleRects(this.problem) // Calculate an effective padding for this specific run based on the detourCount. @@ -71,11 +88,28 @@ export class SingleOverlapSolver extends BaseSolver { solverInput.paddingBuffer + solverInput.detourCount * solverInput.paddingBuffer - const candidates = generateRerouteCandidates({ + let candidates = generateRerouteCandidates({ ...solverInput, paddingBuffer: effectivePadding, // Use the calculated, larger padding }) + // A full-segment shift can hit a remote label on the same net. In that + // case, also try a local turn around the target label's corner. + if ( + candidates.some((path) => + pathEntersAnyNetLabel({ + path, + netLabelPlacements: sameNetLabelsToAvoid, + }), + ) + ) { + candidates = generateRerouteCandidates({ + ...solverInput, + paddingBuffer: effectivePadding, + includeCornerDetours: true, + }) + } + const getLabelOverlapCount = (path: Point[]) => detectTraceLabelOverlap({ traces: [{ ...this.initialTrace, tracePath: path }], @@ -85,6 +119,13 @@ export class SingleOverlapSolver extends BaseSolver { const candidateByPath = new Map() for (const candidate of candidates) { const simplifiedCandidate = simplifyPath(candidate) + if ( + pathEntersAnyNetLabel({ + path: simplifiedCandidate, + netLabelPlacements: sameNetLabelsToAvoid, + }) + ) + continue candidateByPath.set( simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"), simplifiedCandidate, diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts index cb95d0745..cb3a5fb2b 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts @@ -8,6 +8,7 @@ export const generateSnipAndReconnectCandidates = ({ labelBounds, paddingBuffer, detourCount, + includeCornerDetours = false, }: { initialTrace: SolvedTracePath firstInsideIndex: number @@ -15,6 +16,7 @@ export const generateSnipAndReconnectCandidates = ({ labelBounds: any paddingBuffer: number detourCount: number + includeCornerDetours?: boolean }): Point[][] => { if ( firstInsideIndex <= 0 || @@ -91,6 +93,27 @@ export const generateSnipAndReconnectCandidates = ({ ]) } + // Local corner detours keep the exit stem on its original line, instead + // of shifting it all the way to a component pin along the obstacle edge. + if (includeCornerDetours) { + for (const x of [leftX, rightX]) { + for (const y of [bottomY, topY]) { + allCandidateDetours.push( + [ + { x, y: entryPoint.y }, + { x, y }, + { x: exitPoint.x, y }, + ], + [ + { x: entryPoint.x, y }, + { x, y }, + { x, y: exitPoint.y }, + ], + ) + } + } + } + return allCandidateDetours.map((detour) => [ ...pathToEntry, ...detour, diff --git a/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg b/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg index be25608e2..88843fc55 100644 --- a/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +++ b/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg @@ -1,12 +1,12 @@ - U112345678R1SJ1123R2SJ212R3C2C1SJ3123R4JP11234JP2123456U112345678R1SJ1123R2SJ212R3C2C1SJ3123R4JP11234JP2123456XXXX - J_BAT12D_BATU_REG1234C_INC_OUTC_REGU_MCU123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657Y11234C_XINC_XOUTR_RUNC_COREC_MCU1C_MCU2C_MCU3C_MCU4C_MCU5U_FLASH12345678C_FLASHJ_RF12345678C_RFR_CH1LED_CH1R_CH2LED_CH2R_CH3LED_CH3R_CH4LED_CH4R_CH5LED_CH5R_CH6LED_CH6J_PROG123456J_BAT12D_BATU_REG1234C_INC_OUTC_REGU_MCU123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657Y11234C_XINC_XOUTR_RUNC_COREC_MCU1C_MCU2C_MCU3C_MCU4C_MCU5U_FLASH12345678C_FLASHJ_RF12345678C_RFR_CH1LED_CH1R_CH2LED_CH2R_CH3LED_CH3R_CH4LED_CH4R_CH5LED_CH5R_CH6LED_CH6J_PROG123456XXXXXX - CC1CC1USB_DMUSB_DMUSB_DMUSB_DPUSB_DPUSB_DPIO20IO20IO20IO21IO21IO21DTRDTRDTRRTSRTSRTSIO2IO2IO2ENENENENENIO10IO10IO10IO8IO8IO8IO9IO9IO9IO9IO9CC2CC2IO3IO3IO0IO0IO1IO1IO4IO4IO5IO5IO6IO6IO7IO7IO18IO18IO19IO19BASE_ENLED_ACC1CC1USB_DMUSB_DMUSB_DMUSB_DPUSB_DPUSB_DPIO20IO20IO20IO21IO21IO21DTRDTRDTRRTSRTSRTSIO2IO2IO2ENENENENENIO10IO10IO10IO8IO8IO8IO9IO9IO9IO9IO9CC2CC2IO3IO3IO0IO0IO1IO1IO4IO4IO5IO5IO6IO6IO7IO7IO18IO18IO19IO19BASE_ENLED_ASHELL3SHELL4SHELL1SHELL2A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12GNDTXDRXDV3D_POSD_NEG7OUTCTSDSRRIDCDDTRRTSR232VCCVINGNDENNCVOUTGND23GND24GND25GND26GND27GND28GND29GND30GND1GND23V3NC1IO2IO3NC2ENNC3NC4GND3IO0IO1GND4NC5IO10NC6IO4IO5IO6IO7IO8IO9NC7NC8IO18IO19NC9NC10RXD0TXD0NC11NC12NC13NC14GND5GND6GND7GND8GND9GND10GND11GND12GND13GND14GND15GND16GND17GND22GND18GND19GND20GND21BECBEC1212anodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1234567891012345678910XXXXXXXXSHELL3SHELL4SHELL1SHELL2A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12GNDTXDRXDV3D_POSD_NEG7OUTCTSDSRRIDCDDTRRTSR232VCCVINGNDENNCVOUTGND23GND24GND25GND26GND27GND28GND29GND30GND1GND23V3NC1IO2IO3NC2ENNC3NC4GND3IO0IO1GND4NC5IO10NC6IO4IO5IO6IO7IO8IO9NC7NC8IO18IO19NC9NC10RXD0TXD0NC11NC12NC13NC14GND5GND6GND7GND8GND9GND10GND11GND12GND13GND14GND15GND16GND17GND22GND18GND19GND20GND21BECBEC1212anodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1234567891012345678910XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXTYPE-C 16PIN 2MD(073)J1CH340CU2AP2112K-3.3TRG1U3ESP32-C3-MINI-1-H4XU1MMBT3904Q1MMBT3904Q2J2J3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + " fill="rgba(255, 255, 255, 0.6)" stroke="rgb(132, 0, 0)" stroke-width="0.43180223457600003px"/>XXXXXXXXXTYPE-C 16PIN 2MD(073)J1CH340CU2AP2112K-3.3TRG1U3ESP32-C3-MINI-1-H4XU1MMBT3904Q1MMBT3904Q2J2J3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro173-boost-drv8711-ground-label-overlap.snap.svg b/tests/repros/__snapshots__/repro173-boost-drv8711-ground-label-overlap.snap.svg index 651032eba..f507a86cd 100644 --- a/tests/repros/__snapshots__/repro173-boost-drv8711-ground-label-overlap.snap.svg +++ b/tests/repros/__snapshots__/repro173-boost-drv8711-ground-label-overlap.snap.svg @@ -1,6 +1,6 @@ - S1G1S2G2D21D22D11D12S1G1S2G2D21D22D11D12S1G1S2G2D21D22D11D12S1G1S2G2D21D22D11D12GNDVMBOUT2BOUT1AOUT2AOUT1S1G1S2G2D21D22D11D12S1G1S2G2D21D22D11D12S1G1S2G2D21D22D11D12S1G1S2G2D21D22D11D12GNDVMBOUT2BOUT1AOUT2AOUT1 { +test("repro173 BOOST-DRV8711 ground trace clears the R1 GND label", async () => { const solver = new SchematicTracePipelineSolver( inputProblem as unknown as InputProblem, { hideRatsNet: true }, @@ -32,10 +32,17 @@ test("repro173 BOOST-DRV8711 ground trace overlaps the R1 GND label", async () = trace.pinIds.includes("schematic_port_85"), )! - // Current bug: the C1-to-R2 ground rail crosses the rendered GND label below - // R1. Record the observed failure; update this assertion and snapshot when fixed. + // A detour around VM must also keep the C1-to-R2 ground rail clear of + // the GND label below R1, even though both belong to the same net. expect( pathIntersectsRenderedLabel(c1ToR2GroundTrace.tracePath, r1GroundLabel), - ).toBe(true) + ).toBe(false) + expect( + netLabelPlacements + .filter((label) => label.netId === "VM") + .some((label) => + pathIntersectsRenderedLabel(c1ToR2GroundTrace.tracePath, label), + ), + ).toBe(false) await expect(solver).toMatchSolverSnapshot(import.meta.path) }) From 06a82136c12554455c6db4cd3fa784a7dd52be01 Mon Sep 17 00:00:00 2001 From: techmannih Date: Thu, 10 Sep 2026 16:44:39 +0530 Subject: [PATCH 2/3] up --- .../SingleOverlapSolver.ts | 82 +++++++++++++------ .../getCombinedLabelObstacle.ts | 26 ++++++ .../bug-report-20260717T022934Z.snap.svg | 24 +++--- .../bug-report-20260717T022934Z.test.ts | 22 ++++- .../bug-report-20260721T221026Z.snap.svg | 20 ++--- .../bug-report-20260721T221026Z.test.ts | 33 +++++++- .../bug-report-20260905T041712Z.snap.svg | 32 ++++---- 7 files changed, 170 insertions(+), 69 deletions(-) create mode 100644 lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/getCombinedLabelObstacle.ts diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts index 3d70baf71..c6762266a 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts @@ -8,6 +8,7 @@ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSol import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" import { generateRerouteCandidates } from "../../rerouteCollidingTrace" +import { getCombinedLabelObstacle } from "./getCombinedLabelObstacle" import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap" import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces" @@ -110,45 +111,72 @@ export class SingleOverlapSolver extends BaseSolver { }) } + // If local detours cannot clear a neighboring same-net label, also + // generate routes around both label bodies. Merely rejecting those + // detours can leave the trace crossing the original target label. + const blockingLabels = sameNetLabelsToAvoid.filter((label) => + candidates.some((path) => + pathEntersAnyNetLabel({ path, netLabelPlacements: [label] }), + ), + ) + let fallbackCandidates: Point[][] = [] + if (blockingLabels.length > 0) { + fallbackCandidates = generateRerouteCandidates({ + ...solverInput, + label: getCombinedLabelObstacle(this.label, blockingLabels), + paddingBuffer: effectivePadding, + includeCornerDetours: true, + }) + } + const getLabelOverlapCount = (path: Point[]) => detectTraceLabelOverlap({ traces: [{ ...this.initialTrace, tracePath: path }], netLabels: this.netLabelPlacements, }).length - const candidateByPath = new Map() - for (const candidate of candidates) { - const simplifiedCandidate = simplifyPath(candidate) - if ( - pathEntersAnyNetLabel({ - path: simplifiedCandidate, - netLabelPlacements: sameNetLabelsToAvoid, - }) - ) - continue - candidateByPath.set( - simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"), - simplifiedCandidate, - ) - } - - this.queuedCandidatePaths = [...candidateByPath.values()].sort((a, b) => { - const pathLengthDifference = getPathLength(a) - getPathLength(b) - if (Math.abs(pathLengthDifference) >= PATH_LENGTH_EPSILON) { - return pathLengthDifference + const getQueuedCandidates = (paths: Point[][]) => { + const candidateByPath = new Map() + for (const candidate of paths) { + const simplifiedCandidate = simplifyPath(candidate) + if ( + pathEntersAnyNetLabel({ + path: simplifiedCandidate, + netLabelPlacements: sameNetLabelsToAvoid, + }) + ) + continue + candidateByPath.set( + simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"), + simplifiedCandidate, + ) } - const overlapCountDifference = - getLabelOverlapCount(a) - getLabelOverlapCount(b) - if (overlapCountDifference !== 0) return overlapCountDifference + return [...candidateByPath.values()].sort((a, b) => { + const pathLengthDifference = getPathLength(a) - getPathLength(b) + if (Math.abs(pathLengthDifference) >= PATH_LENGTH_EPSILON) { + return pathLengthDifference + } - return a.length - b.length - }) + const overlapCountDifference = + getLabelOverlapCount(a) - getLabelOverlapCount(b) + if (overlapCountDifference !== 0) return overlapCountDifference + + return a.length - b.length + }) + } + // Preserve the normal search first, then exhaust the finite set of wider + // detours. Applying the five-candidate limit to both groups would leave + // those fallback routes untried in crowded power-label layouts. + this.queuedCandidatePaths = [ + ...getQueuedCandidates(candidates).slice(0, MAX_TRIES), + ...getQueuedCandidates(fallbackCandidates), + ] } override _step() { - // Failure conditions: no more candidates or exceeded max tries - if (this.queuedCandidatePaths.length === 0 || this._tried >= MAX_TRIES) { + // Both the local routes and any wider fallback routes have been tried. + if (this.queuedCandidatePaths.length === 0) { this.failed = true return } diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/getCombinedLabelObstacle.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/getCombinedLabelObstacle.ts new file mode 100644 index 000000000..64eefc712 --- /dev/null +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/getCombinedLabelObstacle.ts @@ -0,0 +1,26 @@ +import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver" +import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry" + +/** + * Builds a temporary obstacle for detour generation without moving any label. + * Centers and bounds are points in schematic-world millimeters, with +X right + * and +Y up in the right-handed XY plane. Width and height are axis extents. + */ +export const getCombinedLabelObstacle = ( + targetLabel: NetLabelPlacement, + blockingLabels: NetLabelPlacement[], +): NetLabelPlacement => { + const bounds = [targetLabel, ...blockingLabels].map((label) => + getRectBounds(label.center, label.width, label.height), + ) + const minX = Math.min(...bounds.map((labelBounds) => labelBounds.minX)) + const maxX = Math.max(...bounds.map((labelBounds) => labelBounds.maxX)) + const minY = Math.min(...bounds.map((labelBounds) => labelBounds.minY)) + const maxY = Math.max(...bounds.map((labelBounds) => labelBounds.maxY)) + return { + ...targetLabel, + center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }, + width: maxX - minX, + height: maxY - minY, + } +} diff --git a/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg b/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg index 88843fc55..08532367c 100644 --- a/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +++ b/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg @@ -1,12 +1,12 @@ - U112345678R1SJ1123R2SJ212R3C2C1SJ3123R4JP11234JP2123456U112345678R1SJ1123R2SJ212R3C2C1SJ3123R4JP11234JP2123456XXXX { +test("bug-report-20260717T022934Z", async () => { const solver = new SchematicTracePipelineSolver(inputProblem as any) solver.solve() - expect(solver).toMatchSolverSnapshot(import.meta.path) + const output = solver.netLabelToTraceSolver!.getOutput() + const groundLabel = output.netLabelPlacements.find( + (label) => label.netId === "GND" && label.pinIds.includes("U1.2"), + )! + const groundConnector = output.traces.find( + (trace) => trace.userNetId === "GND" && trace.pinIds.includes("U1.2"), + )! + const pin = groundConnector.pins[0]! + + // Keep the GND rail where it was placed and connect the pin with one elbow, + // without the upward jog caused by leaving the neighboring power trace unrouted. + expect(groundConnector.tracePath).toEqual([ + { x: pin.x, y: pin.y }, + { x: groundLabel.anchorPoint.x, y: pin.y }, + groundLabel.anchorPoint, + ]) + expect(getOutputLabelCollisions(output)).toEqual([]) + await expect(solver).toMatchSolverSnapshot(import.meta.path) }) diff --git a/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg b/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg index 3f2a80cfd..10d55fa58 100644 --- a/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +++ b/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg @@ -1,6 +1,6 @@ - J_BAT12D_BATU_REG1234C_INC_OUTC_REGU_MCU123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657Y11234C_XINC_XOUTR_RUNC_COREC_MCU1C_MCU2C_MCU3C_MCU4C_MCU5U_FLASH12345678C_FLASHJ_RF12345678C_RFR_CH1LED_CH1R_CH2LED_CH2R_CH3LED_CH3R_CH4LED_CH4R_CH5LED_CH5R_CH6LED_CH6J_PROG123456J_BAT12D_BATU_REG1234C_INC_OUTC_REGU_MCU123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657Y11234C_XINC_XOUTR_RUNC_COREC_MCU1C_MCU2C_MCU3C_MCU4C_MCU5U_FLASH12345678C_FLASHJ_RF12345678C_RFR_CH1LED_CH1R_CH2LED_CH2R_CH3LED_CH3R_CH4LED_CH4R_CH5LED_CH5R_CH6LED_CH6J_PROG123456XXXXXX { +test("bug-report-20260721T221026Z", async () => { const solver = new SchematicTracePipelineSolver(inputProblem as any) solver.solve() @@ -19,5 +21,32 @@ test("bug-report-20260721T221026Z", () => { expect( tracePathContainsPoint(alignedTrace.tracePath, attachedLabel.anchorPoint), ).toBe(true) - expect(solver).toMatchSolverSnapshot(import.meta.path) + const output = solver.netLabelToTraceSolver!.getOutput() + const powerTrace = output.traces.find( + (trace) => trace.mspPairId === "U_MCU.48-U_MCU.44", + )! + const neighboringPowerLabel = output.netLabelPlacements.find((label) => + label.pinIds.includes("U_MCU.42"), + )! + + // The V3V3 detour must clear both the DM/DP tags and the neighboring + // V3V3 label. Check actual collisions, not only their total count. + expect(getOutputLabelCollisions(output)).toEqual([]) + expect( + pathEntersAnyNetLabel({ + path: powerTrace.tracePath, + netLabelPlacements: [neighboringPowerLabel], + }), + ).toBe(false) + expect( + output.traces.some( + (trace) => + trace.globalConnNetId === neighboringPowerLabel.globalConnNetId && + tracePathContainsPoint( + trace.tracePath, + neighboringPowerLabel.anchorPoint, + ), + ), + ).toBe(true) + await expect(solver).toMatchSolverSnapshot(import.meta.path) }) diff --git a/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg b/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg index 9bea8d0ad..b57864a0b 100644 --- a/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg +++ b/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg @@ -1,11 +1,11 @@ - CC1CC1USB_DMUSB_DMUSB_DMUSB_DPUSB_DPUSB_DPIO20IO20IO20IO21IO21IO21DTRDTRDTRRTSRTSRTSIO2IO2IO2ENENENENENIO10IO10IO10IO8IO8IO8IO9IO9IO9IO9IO9CC2CC2IO3IO3IO0IO0IO1IO1IO4IO4IO5IO5IO6IO6IO7IO7IO18IO18IO19IO19BASE_ENLED_ACC1CC1USB_DMUSB_DMUSB_DMUSB_DPUSB_DPUSB_DPIO20IO20IO20IO21IO21IO21DTRDTRDTRRTSRTSRTSIO2IO2IO2ENENENENENIO10IO10IO10IO8IO8IO8IO9IO9IO9IO9IO9CC2CC2IO3IO3IO0IO0IO1IO1IO4IO4IO5IO5IO6IO6IO7IO7IO18IO18IO19IO19BASE_ENLED_ASHELL3SHELL4SHELL1SHELL2A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12GNDTXDRXDV3D_POSD_NEG7OUTCTSDSRRIDCDDTRRTSR232VCCVINGNDENNCVOUTGND23GND24GND25GND26GND27GND28GND29GND30GND1GND23V3NC1IO2IO3NC2ENNC3NC4GND3IO0IO1GND4NC5IO10NC6IO4IO5IO6IO7IO8IO9NC7NC8IO18IO19NC9NC10RXD0TXD0NC11NC12NC13NC14GND5GND6GND7GND8GND9GND10GND11GND12GND13GND14GND15GND16GND17GND22GND18GND19GND20GND21BECBEC1212anodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1234567891012345678910XXXXXXXXSHELL3SHELL4SHELL1SHELL2A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12GNDTXDRXDV3D_POSD_NEG7OUTCTSDSRRIDCDDTRRTSR232VCCVINGNDENNCVOUTGND23GND24GND25GND26GND27GND28GND29GND30GND1GND23V3NC1IO2IO3NC2ENNC3NC4GND3IO0IO1GND4NC5IO10NC6IO4IO5IO6IO7IO8IO9NC7NC8IO18IO19NC9NC10RXD0TXD0NC11NC12NC13NC14GND5GND6GND7GND8GND9GND10GND11GND12GND13GND14GND15GND16GND17GND22GND18GND19GND20GND21BECBEC1212anodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1234567891012345678910XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXTYPE-C 16PIN 2MD(073)J1CH340CU2AP2112K-3.3TRG1U3ESP32-C3-MINI-1-H4XU1MMBT3904Q1MMBT3904Q2J2J3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + " fill="rgba(255, 255, 255, 0.6)" stroke="rgb(132, 0, 0)" stroke-width="0.43180223457600003px"/>XXXXXXXXXTYPE-C 16PIN 2MD(073)J1CH340CU2AP2112K-3.3TRG1U3ESP32-C3-MINI-1-H4XU1MMBT3904Q1MMBT3904Q2J2J3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ No newline at end of file From 61459f807fc5296b049f4b4d9c451b337ee98fe9 Mon Sep 17 00:00:00 2001 From: techmannih Date: Thu, 10 Sep 2026 22:09:16 +0530 Subject: [PATCH 3/3] up --- .../AvailableNetOrientationSolver.ts | 88 ++++- .../NetLabelNetLabelCollisionSolver.ts | 20 +- .../bug-report-20260706T213649Z.snap.svg | 26 +- .../bug-report-20260706T220324Z.snap.svg | 183 ++++----- .../bug-report-20260707T092615Z.snap.svg | 20 +- .../bug-report-20260707T134722Z.snap.svg | 124 +++--- .../bug-report-20260716T144856Z.snap.svg | 52 +-- .../bug-report-20260717T022934Z.snap.svg | 33 +- .../bug-report-20260721T221026Z.snap.svg | 52 +-- .../bug-report-20260721T221026Z.test.ts | 11 + .../bug-report-20260728T225606Z.snap.svg | 24 +- .../bug-report-20260804T095800Z.snap.svg | 26 +- .../bug-report-20260901T055358Z.snap.svg | 76 ++-- .../bug-report-20260905T041712Z.snap.svg | 31 +- .../bug-report-20260907T083336Z.snap.svg | 8 +- .../bug-report-20260907T145640Z.snap.svg | 26 +- .../examples/__snapshots__/example39.snap.svg | 46 +-- .../examples/__snapshots__/example41.snap.svg | 20 +- .../examples/__snapshots__/example44.snap.svg | 20 +- .../examples/__snapshots__/example45.snap.svg | 20 +- tests/examples/example41.test.ts | 17 + .../board-1273-trace-overlap-cycle.snap.svg | 52 +-- ...d1096-usb-label-overlap-iteration.snap.svg | 49 +-- .../repro-atmega328p-fault-pullup.snap.svg | 165 ++++---- ...o-atmega328p-missing-gnd-netlabel.snap.svg | 370 +++++++++--------- .../repro-bq24074-battery-charger.snap.svg | 26 +- ...core-ground-inline-label-fallback.snap.svg | 20 +- .../repro-esp12f-section.snap.svg | 36 +- .../repro-hub-usb-sheet.snap.svg | 148 +++---- .../repro-ina237-current-monitor.snap.svg | 47 +-- .../repro-isolated-rs485-isow7841.snap.svg | 20 +- ...use-switch-ground-unnecessary-jog.snap.svg | 37 +- .../repro-pmp11282-isolated-dcdc.snap.svg | 20 +- .../repro-robot-controller-imu-tof.snap.svg | 12 +- .../repro-smartwatch-power-sheet.snap.svg | 20 +- .../repro-tida00553-j4-cell-divider.snap.svg | 130 +++--- .../repro-tida010076-page02.snap.svg | 144 +++---- ...repro-usb-power-vbus-label-detour.snap.svg | 80 ++-- .../allowed-orientations.snap.svg | 48 +++ .../allowed-orientations.test.ts | 100 +++++ .../port-only-allowed-orientations.test.ts | 41 ++ .../trace-end-allowed-orientation.test.ts | 51 +++ 42 files changed, 1399 insertions(+), 1140 deletions(-) create mode 100644 tests/solvers/NetLabelNetLabelCollisionSolver/__snapshots__/allowed-orientations.snap.svg create mode 100644 tests/solvers/NetLabelNetLabelCollisionSolver/allowed-orientations.test.ts create mode 100644 tests/solvers/NetLabelNetLabelCollisionSolver/port-only-allowed-orientations.test.ts create mode 100644 tests/solvers/NetLabelNetLabelCollisionSolver/trace-end-allowed-orientation.test.ts diff --git a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts index baba205ed..9f55c0056 100644 --- a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +++ b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts @@ -284,8 +284,8 @@ export class AvailableNetOrientationSolver extends BaseSolver { if (overlapsHorizontalConstraintMismatch) return true } - if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false - + // Even a correctly oriented label may need a connector to clear a neighbor; + // later collision resolution must not rotate it outside its constraints. const bounds = getRectBounds(label.center, label.width, label.height) return this.outputNetLabelPlacements.some((otherLabel, otherIndex) => { if (otherIndex === labelIndex) return false @@ -578,11 +578,88 @@ export class AvailableNetOrientationSolver extends BaseSolver { ) if (shiftedCandidate) return shiftedCandidate - return this.findValidLateralShiftedCandidate( + const lateralCandidate = this.findValidLateralShiftedCandidate( label, orientations[0]!, labelIndex, ) + if (lateralCandidate) return lateralCandidate + + return this.findValidBranchFromHostTrace( + label, + orientations[0]!, + labelIndex, + ) + } + + /** + * Searches connector source points in schematic world coordinates (mm, + * +X right, +Y up). Orientation vectors are directions in the same frame. + */ + private findValidBranchFromHostTrace( + label: NetLabelPlacement, + orientation: FacingDirection, + labelIndex: number, + ): EvaluatedCandidate | null { + const direction = dir(orientation) + const perpendicular = { x: -direction.y, y: direction.x } + const maxDistance = this.getSearchDistanceLimit(label, orientation) + const sources: Point[] = [] + for (const traceId of label.mspConnectionPairIds) { + const trace = this.traceMap[traceId] + if (!trace) continue + for (let i = 0; i < trace.tracePath.length - 1; i++) { + const start = trace.tracePath[i]! + const end = trace.tracePath[i + 1]! + const steps = Math.max( + 1, + Math.ceil( + Math.hypot(end.x - start.x, end.y - start.y) / LABEL_SEARCH_STEP, + ), + ) + for (let step = 0; step <= steps; step++) { + sources.push({ + x: start.x + ((end.x - start.x) * step) / steps, + y: start.y + ((end.y - start.y) * step) / steps, + }) + } + } + } + sources.sort( + (a, b) => + Math.hypot(a.x - label.anchorPoint.x, a.y - label.anchorPoint.y) - + Math.hypot(b.x - label.anchorPoint.x, b.y - label.anchorPoint.y), + ) + // The original anchor can be trapped between labels. Search short branches + // from the existing wire while preserving both connectivity and orientation. + for (const connectorSource of sources) { + for ( + let offset = 0; + offset <= maxDistance + EPS; + offset += LABEL_SEARCH_STEP + ) { + for (const sign of offset === 0 ? [1] : [-1, 1]) { + const candidate = this.findValidCandidateInShiftColumn({ + label, + labelIndex, + orientation, + direction, + baseAnchor: { + x: connectorSource.x + perpendicular.x * offset * sign, + y: connectorSource.y + perpendicular.y * offset * sign, + }, + connectorSource, + maxSearchDistance: maxDistance, + outwardDistance: offset * sign, + phase: "lateral-shift", + startDistance: WICK_CLEARANCE, + stopOnTraceCollision: false, + }) + if (candidate) return candidate + } + } + } + return null } private findValidCrowdedTopVerticalFanoutCandidate( @@ -1519,14 +1596,15 @@ export class AvailableNetOrientationSolver extends BaseSolver { ) } if (orientation === "x-") { + // Labels outside the chip still need room for a short horizontal wick. return Math.max( - 0, + this.getSearchDistanceLimit(label, orientation), ...labelChips.map((chip) => baseAnchor.x - chip.bounds.minX), ) } if (orientation === "x+") { return Math.max( - 0, + this.getSearchDistanceLimit(label, orientation), ...labelChips.map((chip) => chip.bounds.maxX - baseAnchor.x), ) } diff --git a/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts b/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts index 04f586303..0f975a81e 100644 --- a/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +++ b/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts @@ -17,6 +17,7 @@ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatia import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" import { getColorFromString } from "lib/utils/getColorFromString" import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds" +import { getOrientationConstraint } from "lib/utils/getOrientationConstraint" type CandidateStatus = | "ok" @@ -208,6 +209,11 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver { const netLabelWidth = this.netLabelWidthOf(label) const netLabelHeight = this.netLabelHeightOf(label) const candidates: Candidate[] = [] + const alongTraceCandidates: Candidate[] = [] + const orientationConstraint = getOrientationConstraint( + this.inputProblem, + label, + ) const buildCandidate = ( orientation: FacingDirection, @@ -295,12 +301,24 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver { ), ) } + // A constrained label can also extend past a trace endpoint. Keep + // its host segment in collision checks so it cannot cover the wire. + for (const orientation of orientationConstraint ?? []) { + if (perpendicularOrientations.includes(orientation)) continue + alongTraceCandidates.push( + buildCandidate(orientation, anchor, mspPairId), + ) + } } } } } - return candidates + return orientationConstraint === null + ? candidates + : [...candidates, ...alongTraceCandidates].filter((candidate) => + orientationConstraint.includes(candidate.orientation), + ) } private checkCandidate( 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 ad215a434..d1417e75c 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,9 +1,9 @@ - J112345678J212345678J3123456U112345678U212345678U312345678U412345678U512345678U612345678U712345678U812345678R1R2R3R4R5R6R7R8R9R10R11R12C1C2C3C4C5C6C7C8D1D2D3D4J112345678J212345678J3123456U112345678U212345678U312345678U412345678U512345678U612345678U712345678U812345678R1R2R3R4R5R6R7R8R9R10R11R12C1C2C3C4C5C6C7C8D1D2D3D4XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - \ No newline at end of file diff --git a/tests/solvers/NetLabelNetLabelCollisionSolver/allowed-orientations.test.ts b/tests/solvers/NetLabelNetLabelCollisionSolver/allowed-orientations.test.ts new file mode 100644 index 000000000..f5cae7aaf --- /dev/null +++ b/tests/solvers/NetLabelNetLabelCollisionSolver/allowed-orientations.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from "bun:test" +import { getSvgFromGraphicsObject } from "graphics-debug" +import { NetLabelNetLabelCollisionSolver } from "lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver" +import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" + +test("moves a power label along its trace without flipping its required orientation", async () => { + const inputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: -2, y: -0.5 }, + width: 0.4, + height: 0.8, + pins: [{ pinId: "U1.1", displayName: "VDD", x: -2, y: 0 }], + }, + { + chipId: "U2", + center: { x: 2, y: -0.5 }, + width: 0.4, + height: 0.8, + pins: [{ pinId: "U2.1", displayName: "VDD", x: 2, y: 0 }], + }, + ], + directConnections: [], + netConnections: [{ netId: "V1V1", pinIds: ["U1.1", "U2.1"] }], + availableNetLabelOrientations: { V1V1: ["y+"] }, + } + const trace: SolvedTracePath = { + mspPairId: "U1.1-U2.1", + globalConnNetId: "power-connectivity", + dcConnNetId: "power-connectivity", + userNetId: "V1V1", + pins: [ + { ...inputProblem.chips[0]!.pins[0]!, chipId: "U1" }, + { ...inputProblem.chips[1]!.pins[0]!, chipId: "U2" }, + ], + pinIds: ["U1.1", "U2.1"], + mspConnectionPairIds: ["U1.1-U2.1"], + tracePath: [ + { x: -2, y: 0 }, + { x: 2, y: 0 }, + ], + } + const powerLabel: NetLabelPlacement = { + globalConnNetId: trace.globalConnNetId, + // Resolve the constraint through the connected pins, even without netId. + netLabelText: "V1V1", + pinIds: trace.pinIds, + mspConnectionPairIds: [trace.mspPairId], + orientation: "y+", + anchorPoint: { x: -2, y: 0 }, + center: { x: -2, y: 0.2 }, + width: 0.6, + height: 0.4, + } + const obstacle: NetLabelPlacement = { + globalConnNetId: "signal-connectivity", + netLabelText: "BLOCKED", + pinIds: [], + mspConnectionPairIds: [], + orientation: "x+", + anchorPoint: { x: -3, y: 0.2 }, + center: { x: -2, y: 0.2 }, + width: 2, + height: 0.4, + } + const solver = new NetLabelNetLabelCollisionSolver({ + inputProblem, + traces: [trace], + netLabelPlacements: [powerLabel], + fixedNetLabelPlacements: [obstacle], + }) + solver.solve() + + const placedLabel = solver.getOutput().netLabelPlacements[0]! + expect(placedLabel.orientation).toBe("y+") + expect( + placedLabel.anchorPoint.x - placedLabel.width / 2, + ).toBeGreaterThanOrEqual(-1) + expect(placedLabel.anchorPoint.y).toBe(0) + const nearbyPlacements = solver.getNearbyValidPlacements(powerLabel, 4) + expect(nearbyPlacements.length).toBeGreaterThan(0) + expect(nearbyPlacements.every((label) => label.orientation === "y+")).toBe( + true, + ) + const graphics = solver.visualize() + graphics.rects!.push({ + center: obstacle.center, + width: obstacle.width, + height: obstacle.height, + fill: "rgba(255, 0, 0, 0.15)", + stroke: "red", + label: "Fixed signal label", + }) + await expect( + getSvgFromGraphicsObject(graphics, { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/solvers/NetLabelNetLabelCollisionSolver/port-only-allowed-orientations.test.ts b/tests/solvers/NetLabelNetLabelCollisionSolver/port-only-allowed-orientations.test.ts new file mode 100644 index 000000000..6298bb844 --- /dev/null +++ b/tests/solvers/NetLabelNetLabelCollisionSolver/port-only-allowed-orientations.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test" +import { NetLabelNetLabelCollisionSolver } from "lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver" +import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { FacingDirection } from "lib/utils/dir" + +test("respects ground, signal, empty and missing constraints for port-only placements", () => { + const label: NetLabelPlacement = { + netId: "NET", + globalConnNetId: "connectivity", + pinIds: ["U1.1"], + mspConnectionPairIds: [], + orientation: "y-", + anchorPoint: { x: 0, y: 0 }, + center: { x: 0, y: -0.2 }, + width: 0.6, + height: 0.4, + } + for (const orientations of [ + ["y-"], + ["x-", "x+"], + [], + undefined, + ] satisfies Array) { + const inputProblem: InputProblem = { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: orientations ? { NET: orientations } : {}, + } + const solver = new NetLabelNetLabelCollisionSolver({ + inputProblem, + traces: [], + netLabelPlacements: [label], + }) + const placements = solver.getNearbyValidPlacements(label, 1) + expect(placements.map((placement) => placement.orientation).sort()).toEqual( + [...(orientations ?? ["x+", "x-", "y+", "y-"])].sort(), + ) + } +}) diff --git a/tests/solvers/NetLabelNetLabelCollisionSolver/trace-end-allowed-orientation.test.ts b/tests/solvers/NetLabelNetLabelCollisionSolver/trace-end-allowed-orientation.test.ts new file mode 100644 index 000000000..bf4ab69cc --- /dev/null +++ b/tests/solvers/NetLabelNetLabelCollisionSolver/trace-end-allowed-orientation.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "bun:test" +import { NetLabelNetLabelCollisionSolver } from "lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver" +import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +test("places a horizontal signal label past the trace endpoint without covering its host wire", () => { + const trace: SolvedTracePath = { + mspPairId: "U1.1-U2.1", + globalConnNetId: "signal-connectivity", + dcConnNetId: "signal-connectivity", + userNetId: "SIGNAL", + pins: [ + { chipId: "U1", pinId: "U1.1", x: 0, y: 0 }, + { chipId: "U2", pinId: "U2.1", x: 2, y: 0 }, + ], + pinIds: ["U1.1", "U2.1"], + mspConnectionPairIds: ["U1.1-U2.1"], + tracePath: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], + } + const label: NetLabelPlacement = { + netId: "SIGNAL", + globalConnNetId: trace.globalConnNetId, + pinIds: trace.pinIds, + mspConnectionPairIds: [trace.mspPairId], + orientation: "x+", + anchorPoint: { x: 0, y: 0 }, + center: { x: 0.5, y: 0 }, + width: 1, + height: 0.2, + } + const solver = new NetLabelNetLabelCollisionSolver({ + inputProblem: { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: { SIGNAL: ["x+"] }, + }, + traces: [trace], + netLabelPlacements: [label], + fixedNetLabelPlacements: [{ ...label, globalConnNetId: "obstacle" }], + }) + solver.solve() + + const placed = solver.getOutput().netLabelPlacements[0]! + expect(placed.orientation).toBe("x+") + expect(placed.anchorPoint).toEqual({ x: 2, y: 0 }) + expect(placed.center.x - placed.width / 2).toBeGreaterThan(2) +})