From 2958c60803e427611b9f0dabdaaee07eec1b4b3d Mon Sep 17 00:00:00 2001 From: vinaykrsinghal-stage <253648342+vinaykrsinghal-stage@users.noreply.github.com> Date: Sat, 16 May 2026 18:01:21 +0530 Subject: [PATCH] Coalesce same-net trace segments --- .../TraceCleanupSolver/TraceCleanupSolver.ts | 20 +- .../coalesceSameNetTraces.ts | 427 ++++++++++++++++++ .../examples/__snapshots__/example02.snap.svg | 32 +- .../examples/__snapshots__/example13.snap.svg | 56 +-- .../examples/__snapshots__/example14.snap.svg | 54 +-- .../examples/__snapshots__/example15.snap.svg | 78 ++-- .../examples/__snapshots__/example18.snap.svg | 32 +- .../examples/__snapshots__/example19.snap.svg | 16 +- .../examples/__snapshots__/example21.snap.svg | 50 +- .../examples/__snapshots__/example29.snap.svg | 182 +++----- .../coalesceSameNetTraces.test.ts | 107 +++++ 11 files changed, 735 insertions(+), 319 deletions(-) create mode 100644 lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts create mode 100644 tests/solvers/TraceCleanupSolver/coalesceSameNetTraces.test.ts diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca..c0ccf138 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -20,6 +20,7 @@ interface TraceCleanupSolverInput { import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" import { is4PointRectangle } from "./is4PointRectangle" +import { coalesceSameNetTraces } from "./coalesceSameNetTraces" /** * Represents the different stages or steps within the trace cleanup pipeline. @@ -27,6 +28,7 @@ import { is4PointRectangle } from "./is4PointRectangle" type PipelineStep = | "minimizing_turns" | "balancing_l_shapes" + | "coalescing_same_net_traces" | "untangling_traces" /** @@ -34,7 +36,8 @@ type PipelineStep = * It operates in a multi-step pipeline: * 1. **Untangling Traces**: It first attempts to untangle any overlapping or highly convoluted traces using a sub-solver. * 2. **Minimizing Turns**: After untangling, it iterates through each trace to minimize the number of turns, simplifying their paths. - * 3. **Balancing L-Shapes**: Finally, it balances L-shaped trace segments to create more visually appealing and consistent layouts. + * 3. **Balancing L-Shapes**: It balances L-shaped trace segments to create more visually appealing and consistent layouts. + * 4. **Coalescing Same-Net Traces**: Finally, it snaps close, overlapping same-net internal segments onto shared runs. * The solver processes traces one by one, applying these cleanup steps sequentially to refine the overall trace layout. */ export class TraceCleanupSolver extends BaseSolver { @@ -84,6 +87,9 @@ export class TraceCleanupSolver extends BaseSolver { case "balancing_l_shapes": this._runBalanceLShapesStep() break + case "coalescing_same_net_traces": + this._runCoalesceSameNetTracesStep() + break } } @@ -108,13 +114,23 @@ export class TraceCleanupSolver extends BaseSolver { private _runBalanceLShapesStep() { if (this.traceIdQueue.length === 0) { - this.solved = true + this.pipelineStep = "coalescing_same_net_traces" return } this._processTrace("balancing_l_shapes") } + private _runCoalesceSameNetTracesStep() { + const { traces, coalescedSegmentCount } = coalesceSameNetTraces( + this.outputTraces, + ) + this.outputTraces = traces + this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) + this.stats.coalescedSameNetSegmentCount = coalescedSegmentCount + this.solved = true + } + private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") { const targetMspConnectionPairId = this.traceIdQueue.shift()! this.activeTraceId = targetMspConnectionPairId diff --git a/lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts b/lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts new file mode 100644 index 00000000..3d0bf251 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts @@ -0,0 +1,427 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { + isHorizontal, + isVertical, +} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" +import { simplifyPath } from "./simplifyPath" + +const EPS = 1e-6 + +type Orientation = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + segmentIndex: number + orientation: Orientation + coord: number + start: number + end: number + length: number + isEndpointSegment: boolean + isInternalSegment: boolean + globalConnNetId: string +} + +export interface CoalesceSameNetTracesResult { + traces: SolvedTracePath[] + coalescedSegmentCount: number +} + +const cloneTrace = (trace: SolvedTracePath): SolvedTracePath => ({ + ...trace, + tracePath: trace.tracePath.map((p) => ({ x: p.x, y: p.y })), +}) + +const getSegmentRef = ( + trace: SolvedTracePath, + traceIndex: number, + segmentIndex: number, +): SegmentRef | null => { + const p1 = trace.tracePath[segmentIndex] + const p2 = trace.tracePath[segmentIndex + 1] + if (!p1 || !p2) return null + + const isInternalSegment = + segmentIndex > 0 && segmentIndex < trace.tracePath.length - 2 + const isEndpointSegment = + segmentIndex === 0 || segmentIndex === trace.tracePath.length - 2 + + if (isHorizontal(p1, p2, EPS)) { + const start = Math.min(p1.x, p2.x) + const end = Math.max(p1.x, p2.x) + return { + traceIndex, + segmentIndex, + orientation: "horizontal", + coord: p1.y, + start, + end, + length: end - start, + isEndpointSegment, + isInternalSegment, + globalConnNetId: trace.globalConnNetId, + } + } + + if (isVertical(p1, p2, EPS)) { + const start = Math.min(p1.y, p2.y) + const end = Math.max(p1.y, p2.y) + return { + traceIndex, + segmentIndex, + orientation: "vertical", + coord: p1.x, + start, + end, + length: end - start, + isEndpointSegment, + isInternalSegment, + globalConnNetId: trace.globalConnNetId, + } + } + + return null +} + +const collectSegments = (traces: SolvedTracePath[]): SegmentRef[] => { + const segments: SegmentRef[] = [] + traces.forEach((trace, traceIndex) => { + for ( + let segmentIndex = 0; + segmentIndex < trace.tracePath.length - 1; + segmentIndex++ + ) { + const segment = getSegmentRef(trace, traceIndex, segmentIndex) + if (segment && segment.length > EPS) { + segments.push(segment) + } + } + }) + return segments +} + +const getOverlapLength = (a: SegmentRef, b: SegmentRef): number => + Math.min(a.end, b.end) - Math.max(a.start, b.start) + +const getOverlapRange = (a: SegmentRef, b: SegmentRef) => ({ + start: Math.max(a.start, b.start), + end: Math.min(a.end, b.end), +}) + +const getPathWithSegmentCoord = ( + path: Point[], + segmentIndex: number, + orientation: Orientation, + coord: number, +): Point[] => + path.map((point, index) => { + if (index !== segmentIndex && index !== segmentIndex + 1) { + return { x: point.x, y: point.y } + } + return orientation === "horizontal" + ? { x: point.x, y: coord } + : { x: coord, y: point.y } + }) + +const getMajorCoord = (point: Point, orientation: Orientation) => + orientation === "horizontal" ? point.x : point.y + +const withMajorCoord = ( + point: Point, + orientation: Orientation, + coord: number, +): Point => + orientation === "horizontal" + ? { x: coord, y: point.y } + : { x: point.x, y: coord } + +const removeConsecutiveDuplicatePoints = (path: Point[]) => { + const deduped: Point[] = [] + for (const point of path) { + if ( + deduped.length === 0 || + !samePoint(deduped[deduped.length - 1]!, point) + ) { + deduped.push(point) + } + } + return deduped +} + +const getPathWithTrimmedEndpointOverlap = ( + trace: SolvedTracePath, + segment: SegmentRef, + overlap: { start: number; end: number }, +): Point[] | null => { + if (!segment.isEndpointSegment || trace.tracePath.length <= 2) return null + + const endpointIndex = + segment.segmentIndex === 0 ? 0 : segment.segmentIndex + 1 + const neighborIndex = segment.segmentIndex === 0 ? 1 : segment.segmentIndex + const endpoint = trace.tracePath[endpointIndex] + const neighbor = trace.tracePath[neighborIndex] + if (!endpoint || !neighbor) return null + + const endpointMajor = getMajorCoord(endpoint, segment.orientation) + const neighborMajor = getMajorCoord(neighbor, segment.orientation) + let newEndpointMajor: number | null = null + + if (endpointMajor < neighborMajor && overlap.start <= endpointMajor + EPS) { + newEndpointMajor = Math.min(overlap.end, neighborMajor) + } else if ( + endpointMajor > neighborMajor && + overlap.end >= endpointMajor - EPS + ) { + newEndpointMajor = Math.max(overlap.start, neighborMajor) + } + + if (newEndpointMajor === null) return null + if (Math.abs(newEndpointMajor - endpointMajor) < EPS) return null + + const candidatePath = trace.tracePath.map((point, index) => + index === endpointIndex + ? withMajorCoord(point, segment.orientation, newEndpointMajor) + : { x: point.x, y: point.y }, + ) + const dedupedPath = removeConsecutiveDuplicatePoints(candidatePath) + if (dedupedPath.length < 2) return null + + return simplifyPath(dedupedPath) +} + +const samePoint = (a: Point, b: Point) => + Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS + +const pointOnSegment = (point: Point, a: Point, b: Point) => { + if (isHorizontal(a, b, EPS)) { + return ( + Math.abs(point.y - a.y) < EPS && + point.x >= Math.min(a.x, b.x) - EPS && + point.x <= Math.max(a.x, b.x) + EPS + ) + } + if (isVertical(a, b, EPS)) { + return ( + Math.abs(point.x - a.x) < EPS && + point.y >= Math.min(a.y, b.y) - EPS && + point.y <= Math.max(a.y, b.y) + EPS + ) + } + return false +} + +const endpointOnlyTouch = ( + point: Point, + a1: Point, + a2: Point, + b1: Point, + b2: Point, +) => + (samePoint(point, a1) || samePoint(point, a2)) && + (samePoint(point, b1) || samePoint(point, b2)) + +const axisAlignedSegmentsIntersect = ( + a1: Point, + a2: Point, + b1: Point, + b2: Point, +): boolean => { + const aHorizontal = isHorizontal(a1, a2, EPS) + const aVertical = isVertical(a1, a2, EPS) + const bHorizontal = isHorizontal(b1, b2, EPS) + const bVertical = isVertical(b1, b2, EPS) + if ((!aHorizontal && !aVertical) || (!bHorizontal && !bVertical)) return false + + if (aHorizontal && bHorizontal) { + if (Math.abs(a1.y - b1.y) >= EPS) return false + return ( + Math.min(Math.max(a1.x, a2.x), Math.max(b1.x, b2.x)) - + Math.max(Math.min(a1.x, a2.x), Math.min(b1.x, b2.x)) > + EPS + ) + } + + if (aVertical && bVertical) { + if (Math.abs(a1.x - b1.x) >= EPS) return false + return ( + Math.min(Math.max(a1.y, a2.y), Math.max(b1.y, b2.y)) - + Math.max(Math.min(a1.y, a2.y), Math.min(b1.y, b2.y)) > + EPS + ) + } + + const horizontalStart = aHorizontal ? a1 : b1 + const horizontalEnd = aHorizontal ? a2 : b2 + const verticalStart = aVertical ? a1 : b1 + const verticalEnd = aVertical ? a2 : b2 + const intersection = { x: verticalStart.x, y: horizontalStart.y } + + if ( + pointOnSegment(intersection, horizontalStart, horizontalEnd) && + pointOnSegment(intersection, verticalStart, verticalEnd) + ) { + return !endpointOnlyTouch(intersection, a1, a2, b1, b2) + } + + return false +} + +const pathCollidesWithDifferentNetTrace = ( + path: Point[], + globalConnNetId: string, + otherTrace: SolvedTracePath, +) => { + if (otherTrace.globalConnNetId === globalConnNetId) return false + + for (let i = 0; i < path.length - 1; i++) { + for (let j = 0; j < otherTrace.tracePath.length - 1; j++) { + if ( + axisAlignedSegmentsIntersect( + path[i]!, + path[i + 1]!, + otherTrace.tracePath[j]!, + otherTrace.tracePath[j + 1]!, + ) + ) { + return true + } + } + } + + return false +} + +const isSafeTracePath = ( + candidatePath: Point[], + candidateNetId: string, + traces: SolvedTracePath[], + candidateTraceIndex: number, +) => + traces.every((trace, traceIndex) => { + if (traceIndex === candidateTraceIndex) return true + return !pathCollidesWithDifferentNetTrace( + candidatePath, + candidateNetId, + trace, + ) + }) + +const pathsEqual = (a: Point[], b: Point[]) => + a.length === b.length && + a.every((point, index) => samePoint(point, b[index]!)) + +export const coalesceSameNetTraces = ( + traces: SolvedTracePath[], + opts: { + maxSnapDistance?: number + minOverlap?: number + maxPasses?: number + } = {}, +): CoalesceSameNetTracesResult => { + const maxSnapDistance = opts.maxSnapDistance ?? 0.25 + const minOverlap = opts.minOverlap ?? 0.05 + const maxPasses = opts.maxPasses ?? 20 + const outputTraces = traces.map(cloneTrace) + let coalescedSegmentCount = 0 + + for (let pass = 0; pass < maxPasses; pass++) { + const segments = collectSegments(outputTraces) + let changedThisPass = false + + for (let i = 0; i < segments.length; i++) { + const a = segments[i]! + for (let j = i + 1; j < segments.length; j++) { + const b = segments[j]! + if (a.traceIndex === b.traceIndex) continue + if (a.globalConnNetId !== b.globalConnNetId) continue + if (a.orientation !== b.orientation) continue + + const coordDistance = Math.abs(a.coord - b.coord) + const overlapLength = getOverlapLength(a, b) + if (overlapLength < minOverlap) continue + + if (coordDistance < EPS) { + const overlap = getOverlapRange(a, b) + const trimCandidates = [a, b] + .filter((segment) => segment.isEndpointSegment) + .sort((left, right) => left.length - right.length) + + for (const moving of trimCandidates) { + const movingTrace = outputTraces[moving.traceIndex]! + const candidatePath = getPathWithTrimmedEndpointOverlap( + movingTrace, + moving, + overlap, + ) + if (!candidatePath) continue + if (pathsEqual(candidatePath, movingTrace.tracePath)) continue + if ( + !isSafeTracePath( + candidatePath, + movingTrace.globalConnNetId, + outputTraces, + moving.traceIndex, + ) + ) { + continue + } + + outputTraces[moving.traceIndex] = { + ...movingTrace, + tracePath: candidatePath, + } + coalescedSegmentCount++ + changedThisPass = true + break + } + + if (changedThisPass) break + continue + } + + if (coordDistance > maxSnapDistance) continue + if (!a.isInternalSegment || !b.isInternalSegment) continue + + const [target, moving] = a.length >= b.length ? [a, b] : [b, a] + const movingTrace = outputTraces[moving.traceIndex]! + const candidatePath = simplifyPath( + getPathWithSegmentCoord( + movingTrace.tracePath, + moving.segmentIndex, + moving.orientation, + target.coord, + ), + ) + + if (pathsEqual(candidatePath, movingTrace.tracePath)) continue + if ( + !isSafeTracePath( + candidatePath, + movingTrace.globalConnNetId, + outputTraces, + moving.traceIndex, + ) + ) { + continue + } + + outputTraces[moving.traceIndex] = { + ...movingTrace, + tracePath: candidatePath, + } + coalescedSegmentCount++ + changedThisPass = true + break + } + if (changedThisPass) break + } + + if (!changedThisPass) break + } + + return { + traces: outputTraces, + coalescedSegmentCount, + } +} diff --git a/tests/examples/__snapshots__/example02.snap.svg b/tests/examples/__snapshots__/example02.snap.svg index 3815fdc0..c705c977 100644 --- a/tests/examples/__snapshots__/example02.snap.svg +++ b/tests/examples/__snapshots__/example02.snap.svg @@ -53,20 +53,16 @@ x+" data-x="1" data-y="-0.1" cx="500.20151295522464" cy="341.11637364700744" r=" x+" data-x="1" data-y="0.1" cx="500.20151295522464" cy="324.189420823755" r="3" fill="hsl(323, 100%, 50%, 0.8)" /> - + - + - + - + @@ -156,22 +152,22 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53 - + - + - + - + @@ -196,23 +192,19 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53 +globalConnNetId: connectivity_net0" data-x="-1.4574283249999997" data-y="1.5274186000000005" x="283.75416992460123" y="184.33736239143013" width="16.926952823252577" height="38.08564385231816" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net1" data-x="-3.0434765500000003" data-y="-0.4250000000000004" x="149.51935252470935" y="349.5798500586337" width="16.926952823252492" height="38.085643852318185" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net1" data-x="1.9148566499999995" data-y="-1.2284186000000008" x="569.1667133165424" y="417.5769937562517" width="16.926952823252577" height="38.08564385231813" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net2" data-x="1.4571549750000001" data-y="0.5249999999999997" x="530.4292400172993" y="269.1768241481843" width="16.926952823252464" height="38.08564385231813" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" /> - + - + - + - + - + - + - + - + @@ -193,7 +185,7 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492 - + @@ -202,13 +194,13 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492 - + - + - + @@ -251,43 +243,35 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492 +globalConnNetId: connectivity_net3" data-x="3.75" data-y="-1.725" x="527.5217643644805" y="450.0058038305282" width="13.000580383052807" height="29.251305861868843" fill="#00000066" stroke="#000000" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net3" data-x="-2.125" data-y="-2.2249999999999996" x="145.6297156123041" y="482.5072547881602" width="13.000580383052835" height="29.251305861868843" fill="#00000066" stroke="#000000" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net4" data-x="1.475" data-y="0.225" x="379.6401625072548" y="323.25014509576323" width="13.000580383052807" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net4" data-x="-1.301" data-y="-0.8490000000000001" x="199.19210679048172" y="393.0632617527569" width="13.000580383052807" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net2" data-x="-2.125" data-y="0.225" x="145.6297156123041" y="323.25014509576323" width="13.000580383052835" height="29.251305861868843" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net0" data-x="-3.75" data-y="2.225" x="39.99999999999997" y="193.24434126523508" width="13.000580383052835" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net0" data-x="2.2990000000000004" data-y="0.276" x="433.20255368543235" y="319.93499709808475" width="13.000580383052863" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net1" data-x="1.96375" data-y="3.225" x="411.41033081834007" y="128.241439349971" width="13.000580383052807" height="29.25130586186887" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.015383928571428571" /> - + - + - + - + - + - + - + - + @@ -147,13 +139,13 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 - + - + @@ -168,7 +160,7 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 - + @@ -196,43 +188,35 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 +globalConnNetId: connectivity_net4" data-x="1.3510000000000002" data-y="-0.5760000000000001" x="423.4707692307693" y="322.24" width="17.230769230769226" height="38.769230769230774" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net4" data-x="-1.2000000000000002" data-y="-2.6750000000000003" x="203.6923076923077" y="503.0769230769231" width="17.230769230769255" height="38.76923076923083" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net1" data-x="-1.6250000000000002" data-y="-0.30000000000000004" x="156.30769230769232" y="309.2307692307692" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net3" data-x="1.6250000000000002" data-y="0.09999999999999998" x="436.3076923076924" y="274.7692307692308" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net5" data-x="-1.3010000000000002" data-y="0.7260000000000001" x="194.99076923076925" y="210.0676923076923" width="17.230769230769255" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net5" data-x="3.2" data-y="0.525" x="582.7692307692308" y="227.3846153846154" width="17.230769230769283" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net0" data-x="-1.5500000000000003" data-y="0.32500000000000007" x="173.53846153846155" y="244.6153846153846" width="17.230769230769226" height="38.769230769230745" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net2" data-x="1.4755000000000003" data-y="-1.0694553500000001" x="434.19692307692316" y="364.7530763076923" width="17.230769230769226" height="38.769230769230774" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> - + - + - + - + - + - + - + - + - + @@ -776,13 +767,13 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 - + - + - + @@ -791,22 +782,22 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 - + - + - + - + - + @@ -821,22 +812,22 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 - + - + - + - + @@ -882,48 +873,39 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 +globalConnNetId: connectivity_net0" data-x="-1.3099999999999998" data-y="1.4250000000000016" x="348.85072655217965" y="311.4927344782034" width="9.863496257155475" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net0" data-x="0.29250000000000076" data-y="6.9300000000000015" x="427.8819903126376" y="40.00000000000006" width="9.863496257155475" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net6" data-x="-1.3099999999999998" data-y="3.0250000000000017" x="348.85072655217965" y="232.58476442095986" width="9.863496257155475" height="22.192866578599762" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net6" data-x="-3.7550000000000003" data-y="4.193333333333335" x="228.26948480845445" y="174.96550711874357" width="9.863496257155447" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net9" data-x="-2.25" data-y="-1.4000000000000004" x="296.327608982827" y="456.97930427124606" width="22.192866578599705" height="9.863496257155475" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net8" data-x="-2.25" data-y="2.200000000000001" x="296.327608982827" y="279.4363716424482" width="22.192866578599705" height="9.863496257155475" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net10" data-x="0.29250000000000076" data-y="4.980000000000002" x="427.8819903126376" y="136.16908850726549" width="9.863496257155475" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net10" data-x="-3.7550000000000003" data-y="2.2433333333333345" x="228.26948480845445" y="271.13459562600906" width="9.863496257155447" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net10" data-x="-2.49" data-y="-3.1250000000000004" x="290.6560986349626" y="535.8872743284896" width="9.863496257155418" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" /> - + - + - + - + - + @@ -139,7 +134,7 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" - + @@ -167,28 +162,23 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" +globalConnNetId: connectivity_net0" data-x="-1.8574283249999997" data-y="0.9762093000000004" x="161.39522395803996" y="196.79342126794842" width="17.905209437554532" height="40.28672123449769" fill="#ef444466" stroke="#ef4444" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net0" data-x="1.5790330374999988" data-y="2.7275814000000005" x="469.0480260561722" y="40" width="17.90520943755456" height="40.28672123449769" fill="#ef444466" stroke="#ef4444" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net1" data-x="-2.31430995" data-y="-0.9762093000000004" x="120.4924180390637" y="371.58574098183345" width="17.905209437554532" height="40.28672123449769" fill="#00000066" stroke="#000000" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net2" data-x="1.982519574999999" data-y="0.85" x="493.97982495355666" y="219.2831969137558" width="40.28672123449769" height="17.905209437554532" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net3" data-x="1.982519574999999" data-y="-2" x="493.97982495355666" y="474.43243139890774" width="40.28672123449769" height="17.90520943755456" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011169933571428573" /> - + - + - + - + @@ -95,13 +91,13 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438 - + - + diff --git a/tests/examples/__snapshots__/example21.snap.svg b/tests/examples/__snapshots__/example21.snap.svg index 85810dff..aa4f4573 100644 --- a/tests/examples/__snapshots__/example21.snap.svg +++ b/tests/examples/__snapshots__/example21.snap.svg @@ -57,36 +57,28 @@ y+" data-x="2.9752723250000006" data-y="0.6000000000000003" cx="345.123488706365 y-" data-x="2.9752723250000006" data-y="-0.4999999999999998" cx="345.1234887063655" cy="307.25530458590003" r="3" fill="hsl(83, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + @@ -167,7 +159,7 @@ orientation: x-" data-x="2.9752723250000006" data-y="-0.5999999999999999" cx="34 - + @@ -201,43 +193,35 @@ orientation: x-" data-x="2.9752723250000006" data-y="-0.5999999999999999" cx="34 +globalConnNetId: connectivity_net3" data-x="-1.551" data-y="-0.801" x="49.58247775496234" y="316.9016655258955" width="12.776637006616482" height="19.16495550992471" fill="#00000066" stroke="#000000" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net3" data-x="1.551" data-y="-0.801" x="247.7481177275838" y="316.9016655258955" width="12.776637006616511" height="19.16495550992471" fill="#00000066" stroke="#000000" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net3" data-x="2.8699999999999997" data-y="-2.65" x="332.0100387862194" y="435.0216746520648" width="12.776637006616511" height="19.164955509924653" fill="#00000066" stroke="#000000" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net1" data-x="-1.6004999999999998" data-y="0.75" x="46.42026009582477" y="217.81884553958474" width="12.776637006616482" height="19.16495550992471" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net1" data-x="2.9752723250000006" data-y="0.9500000000000005" x="338.7351702030573" y="205.04220853296823" width="12.776637006616454" height="19.16495550992471" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net0" data-x="6.2425" data-y="7.771561172376096e-16" x="547.4560803102897" y="262.53707506274236" width="12.776637006616397" height="25.553274013232908" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net0" data-x="1.501" data-y="1.2009999999999998" x="244.5539584759297" y="185.8133698380105" width="12.776637006616482" height="25.553274013232937" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" /> +globalConnNetId: connectivity_net2" data-x="2.675272325000001" data-y="-0.5999999999999999" x="306.79357768651613" y="307.25530458590003" width="38.32991101984936" height="12.776637006616511" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01565357142857143" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -844,7 +798,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -853,16 +807,16 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + - + - + @@ -871,7 +825,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -880,7 +834,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -889,7 +843,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -898,7 +852,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -907,7 +861,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -916,7 +870,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -925,19 +879,19 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + - + - + @@ -946,13 +900,13 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + - + @@ -1086,7 +1040,7 @@ globalConnNetId: connectivity_net5" data-x="-6.262500000000001" data-y="-3.775" +globalConnNetId: connectivity_net6" data-x="-4" data-y="-5.775" x="275.26364065639507" y="194.7878033288731" width="3.578908747488356" height="8.052544681848872" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net10" data-x="-4" data-y="-9.775" x="275.26364065639507" y="266.365978278641" width="3.578908747488356" height="8.052544681848872" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net12" data-x="-4" data-y="-12.225" x="275.26364065639507" y="310.2076104353738" width="3.578908747488356" height="8.052544681848929" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net14" data-x="-4" data-y="-13.775" x="275.26364065639507" y="337.94415322840894" width="3.578908747488356" height="8.052544681848872" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net18" data-x="-4" data-y="-17.775" x="275.26364065639507" y="409.52232817817674" width="3.578908747488356" height="8.052544681848985" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net22" data-x="-4" data-y="-21.775" x="275.26364065639507" y="481.10050312794465" width="3.578908747488356" height="8.052544681848985" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net26" data-x="-4" data-y="-25.775" x="275.26364065639507" y="552.6786780777126" width="3.578908747488356" height="8.052544681848872" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> + ({ + mspPairId, + globalConnNetId, + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [], + pins: [], + }) as unknown as SolvedTracePath + +test("coalesces close overlapping internal same-net segments", () => { + const lower = makeTrace("lower", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]) + const upper = makeTrace("upper", "net1", [ + { x: 0, y: 2 }, + { x: 0, y: 1.12 }, + { x: 4, y: 1.12 }, + { x: 4, y: 2 }, + ]) + + const result = coalesceSameNetTraces([lower, upper]) + + expect(result.coalescedSegmentCount).toBe(1) + expect(result.traces[1]!.tracePath).toEqual([ + { x: 0, y: 2 }, + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 2 }, + ]) +}) + +test("trims redundant endpoint overlap already covered by same-net trace", () => { + const shortBranch = makeTrace("short", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 2 }, + { x: 1, y: 2 }, + ]) + const trunk = makeTrace("trunk", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 5 }, + { x: 1, y: 5 }, + ]) + + const result = coalesceSameNetTraces([shortBranch, trunk]) + + expect(result.coalescedSegmentCount).toBe(1) + expect(result.traces[0]!.tracePath).toEqual([ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + ]) +}) + +test("does not coalesce close overlapping segments from different nets", () => { + const lower = makeTrace("lower", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]) + const upper = makeTrace("upper", "net2", [ + { x: 0, y: 2 }, + { x: 0, y: 1.12 }, + { x: 4, y: 1.12 }, + { x: 4, y: 2 }, + ]) + + const result = coalesceSameNetTraces([lower, upper]) + + expect(result.coalescedSegmentCount).toBe(0) + expect(result.traces[1]!.tracePath).toEqual(upper.tracePath) +}) + +test("rejects same-net coalescing that would cross a different net", () => { + const lower = makeTrace("lower", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]) + const upper = makeTrace("upper", "net1", [ + { x: 0, y: 2 }, + { x: 0, y: 1.12 }, + { x: 4, y: 1.12 }, + { x: 4, y: 2 }, + ]) + const blocker = makeTrace("blocker", "net2", [ + { x: 2, y: 0.9 }, + { x: 2, y: 1.05 }, + ]) + + const result = coalesceSameNetTraces([lower, upper, blocker]) + + expect(result.coalescedSegmentCount).toBe(0) + expect(result.traces[1]!.tracePath).toEqual(upper.tracePath) +})