From 2bdc642be7575a4bb5df0f2a8765bbcce33e7a28 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Thu, 27 Aug 2026 10:37:45 +0200 Subject: [PATCH] Separate visually merged component-pin traces --- .../TraceOverlapShiftSolver.ts | 66 +- .../board-1273-trace-overlap-cycle.snap.svg | 20 +- .../repro-pmp11282-isolated-dcdc.snap.svg | 2470 ++++++++--------- .../repro-ti-power-output-section.snap.svg | 36 +- ...a01141-comparator-input-clearance.snap.svg | 98 + .../repro48-555-timer-vcc-rail-label.snap.svg | 4 +- .../board-1273-trace-overlap-cycle.test.ts | 2 +- ...da01141-comparator-input-clearance.test.ts | 135 + 8 files changed, 1556 insertions(+), 1275 deletions(-) create mode 100644 tests/repros/__snapshots__/repro-tida01141-comparator-input-clearance.snap.svg create mode 100644 tests/repros/repro-tida01141-comparator-input-clearance.test.ts diff --git a/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts b/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts index 8a54dd22c..31ce514e2 100644 --- a/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +++ b/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts @@ -1,19 +1,24 @@ +import type { ConnectivityMap } from "connectivity-map" import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" -import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" import type { InputProblem } from "lib/types/InputProblem" +import { SCHEMATIC_TRACE_STROKE_WIDTH } from "lib/utils/doesPathCoincideWithTraces" +import type { MspConnectionPairId } from "../MspConnectionPairSolver/MspConnectionPairSolver" import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" -import type { ConnectivityMap } from "connectivity-map" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" import { - TraceOverlapIssueSolver, type OverlappingTraceSegmentLocator, type TraceInteractionKind, + TraceOverlapIssueSolver, } from "./TraceOverlapIssueSolver/TraceOverlapIssueSolver" -import type { MspConnectionPairId } from "../MspConnectionPairSolver/MspConnectionPairSolver" type ConnNetId = string type TraceState = Record const TRACE_STATE_POSITION_EPSILON = 1e-6 +// At half a stroke width or less, antialiasing makes two different-net traces +// look like one continuous wire and can visually imply a false junction. +const MAX_VISUALLY_MERGED_CENTERLINE_SEPARATION = + SCHEMATIC_TRACE_STROKE_WIDTH / 2 + TRACE_STATE_POSITION_EPSILON /** * This solver finds traces that overlap or meet collinearly and aren't @@ -211,6 +216,17 @@ export class TraceOverlapShiftSolver extends BaseSolver { }) } + const pathsTerminateOnSameChip = ( + pathA: SolvedTracePath, + pathB: SolvedTracePath, + ) => + pathA.pins.some((pinA) => + pathB.pins.some( + (pinB) => + pinA.chipId === pinB.chipId && pinA.pinId !== pinB.pinId, + ), + ) + for (let pa = 0; pa < pathsA.length; pa++) { const pathA = pathsA[pa]! const ptsA = pathA.tracePath @@ -238,25 +254,57 @@ export class TraceOverlapShiftSolver extends BaseSolver { const bHorz = Math.abs(b1.y - b2.y) < EPS if (!bVert && !bHorz) continue - // Only consider collinear, parallel interactions. + // Parallel centerlines can still merge visually before they + // are mathematically collinear because schematic traces have + // a non-zero rendered stroke width. if (aVert && bVert) { - if (Math.abs(a1.x - b1.x) < EPS) { + const centerlineSeparation = Math.abs(a1.x - b1.x) + if ( + centerlineSeparation < EPS || + (centerlineSeparation < + MAX_VISUALLY_MERGED_CENTERLINE_SEPARATION && + pathsTerminateOnSameChip(pathA, pathB)) + ) { + const rangeOverlap = getRangeOverlap1D( + a1.y, + a2.y, + b1.y, + b2.y, + ) + if (rangeOverlap <= EPS && centerlineSeparation >= EPS) { + continue + } recordCollinearInteraction({ pathAIndex: pa, segmentAIndex: sa, pathBIndex: pb, segmentBIndex: sb, - rangeOverlap: getRangeOverlap1D(a1.y, a2.y, b1.y, b2.y), + rangeOverlap, }) } } else if (aHorz && bHorz) { - if (Math.abs(a1.y - b1.y) < EPS) { + const centerlineSeparation = Math.abs(a1.y - b1.y) + if ( + centerlineSeparation < EPS || + (centerlineSeparation < + MAX_VISUALLY_MERGED_CENTERLINE_SEPARATION && + pathsTerminateOnSameChip(pathA, pathB)) + ) { + const rangeOverlap = getRangeOverlap1D( + a1.x, + a2.x, + b1.x, + b2.x, + ) + if (rangeOverlap <= EPS && centerlineSeparation >= EPS) { + continue + } recordCollinearInteraction({ pathAIndex: pa, segmentAIndex: sa, pathBIndex: pb, segmentBIndex: sb, - rangeOverlap: getRangeOverlap1D(a1.x, a2.x, b1.x, b2.x), + rangeOverlap, }) } } diff --git a/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg b/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg index c7b4f4497..05e5c0246 100644 --- a/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +++ b/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg @@ -1,6 +1,6 @@ - XXXXXXXXXXX - + + + R11U2A12OUTC3R12XXXXXXX + + \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg b/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg index 88c39f216..4b551ddd9 100644 --- a/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +++ b/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg @@ -1,6 +1,6 @@ - { expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(solver.traceOverlapShiftSolver?.iterations).toBe(27) + expect(solver.traceOverlapShiftSolver?.iterations).toBe(29) expect(solver.postLabelTraceOverlapShiftSolver?.iterations).toBe(1) await expect(solver).toMatchSolverSnapshot(import.meta.path) }) diff --git a/tests/repros/repro-tida01141-comparator-input-clearance.test.ts b/tests/repros/repro-tida01141-comparator-input-clearance.test.ts new file mode 100644 index 000000000..73689e10e --- /dev/null +++ b/tests/repros/repro-tida01141-comparator-input-clearance.test.ts @@ -0,0 +1,135 @@ +import { expect, test } from "bun:test" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" +import type { InputProblem } from "lib/types/InputProblem" +import { SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE } from "lib/utils/doesPathCoincideWithTraces" +import "tests/fixtures/matcher" + +const inputProblem: InputProblem = { + chips: [ + { + chipId: "R11", + center: { x: 3.445, y: 2.6 }, + width: 1.09, + height: 0.6, + pins: [ + { pinId: "R11.1", x: 3.35, y: 2.9, _facingDirection: "y+" }, + { pinId: "R11.2", x: 3.35, y: 2.3, _facingDirection: "y-" }, + ], + }, + { + chipId: "U2A", + center: { x: 4.8, y: 3 }, + width: 1, + height: 0.78, + pins: [ + { pinId: "U2A.1", x: 4.3, y: 3.13, _facingDirection: "x-" }, + { pinId: "U2A.2", x: 4.3, y: 2.86, _facingDirection: "x-" }, + { pinId: "U2A.OUT", x: 5.3, y: 2.99, _facingDirection: "x+" }, + ], + }, + { + chipId: "C3", + center: { x: 2.5925, y: 2.56 }, + width: 1.285, + height: 0.76, + pins: [ + { pinId: "C3.1", x: 2.4, y: 2.94, _facingDirection: "y+" }, + { pinId: "C3.2", x: 2.4, y: 2.18, _facingDirection: "y-" }, + ], + }, + { + chipId: "R12", + center: { x: 4.8, y: 2.3 }, + width: 0.72, + height: 0.68, + pins: [ + { pinId: "R12.1", x: 4.44, y: 2.3, _facingDirection: "x-" }, + { pinId: "R12.2", x: 5.16, y: 2.3, _facingDirection: "x+" }, + ], + }, + ], + directConnections: [ + { netId: "R11 feedback", pinIds: ["R11.1", "U2A.1"] }, + { netId: "R11 feedback", pinIds: ["R11.1", "R12.1"] }, + { netId: "alert feedback", pinIds: ["R12.2", "U2A.OUT"] }, + ], + netConnections: [ + { + netId: "LTV", + netLabelWidth: 0.48, + pinIds: ["C3.1", "U2A.2"], + }, + ], + textBoxes: [], + availableNetLabelOrientations: { LTV: ["x-", "x+"] }, + maxMspPairDistance: 4, + _hideRatsNet: false, +} + +const getParallelCenterlineSeparation = ( + firstTrace: SolvedTracePath, + secondTrace: SolvedTracePath, +) => { + let closestSeparation = Number.POSITIVE_INFINITY + + for ( + let firstIndex = 0; + firstIndex < firstTrace.tracePath.length - 1; + firstIndex++ + ) { + const firstStart = firstTrace.tracePath[firstIndex]! + const firstEnd = firstTrace.tracePath[firstIndex + 1]! + const firstHorizontal = Math.abs(firstStart.y - firstEnd.y) < 1e-6 + if (!firstHorizontal) continue + + for ( + let secondIndex = 0; + secondIndex < secondTrace.tracePath.length - 1; + secondIndex++ + ) { + const secondStart = secondTrace.tracePath[secondIndex]! + const secondEnd = secondTrace.tracePath[secondIndex + 1]! + const secondHorizontal = Math.abs(secondStart.y - secondEnd.y) < 1e-6 + if (!secondHorizontal) continue + + const overlap = + Math.min( + Math.max(firstStart.x, firstEnd.x), + Math.max(secondStart.x, secondEnd.x), + ) - + Math.max( + Math.min(firstStart.x, firstEnd.x), + Math.min(secondStart.x, secondEnd.x), + ) + if (overlap <= 1e-6) continue + + closestSeparation = Math.min( + closestSeparation, + Math.abs(firstStart.y - secondStart.y), + ) + } + } + + return closestSeparation +} + +test("TIDA-01141 comparator input traces keep visible clearance", () => { + const solver = new SchematicTracePipelineSolver(inputProblem) + solver.solve() + + const traces = solver.inlineNetLabelSolver!.getOutput().traces + const feedbackTrace = traces.find( + (trace) => + trace.mspPairId.includes("R11.1") && trace.mspPairId.includes("U2A.1"), + )! + const thresholdTrace = traces.find( + (trace) => + trace.mspPairId.includes("C3.1") && trace.mspPairId.includes("U2A.2"), + )! + + expect( + getParallelCenterlineSeparation(feedbackTrace, thresholdTrace), + ).toBeGreaterThanOrEqual(SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE) + expect(solver).toMatchSolverSnapshot(import.meta.path) +})