From deecac9b5e8fd38e62e030fb2a51d146088fe563 Mon Sep 17 00:00:00 2001 From: Milo Date: Thu, 21 May 2026 04:16:56 +0000 Subject: [PATCH 1/2] fix: remove zero-length trace segments from duplicate consecutive points in simplifyPath Root cause: _applyBestRoute() in UntangleTraceSubsolver splices rerouted segments into the original trace path, which can produce consecutive duplicate boundary points. These zero-length segments survive the collinear-collapse pass and render as extra trace lines in the schematic. Fix: Added removeDuplicateConsecutivePoints() to strip identical adjacent points before each collinear-collapse pass in simplifyPath(). The function runs dedup-collapse-dedup-collapse-dedup to ensure all zero-length segments are eliminated regardless of ordering. Tests: 8 new unit tests for duplicate point removal. Updated example35 snapshot (35 fewer SVG lines, confirming extra trace removal). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../TraceCleanupSolver/simplifyPath.ts | 52 ++++---- .../examples/__snapshots__/example35.snap.svg | 87 ++++--------- .../TraceCleanupSolver/simplifyPath.test.ts | 119 ++++++++++++++++++ 3 files changed, 175 insertions(+), 83 deletions(-) create mode 100644 tests/solvers/TraceCleanupSolver/simplifyPath.test.ts diff --git a/lib/solvers/TraceCleanupSolver/simplifyPath.ts b/lib/solvers/TraceCleanupSolver/simplifyPath.ts index e17bfb52c..d5929f1b4 100644 --- a/lib/solvers/TraceCleanupSolver/simplifyPath.ts +++ b/lib/solvers/TraceCleanupSolver/simplifyPath.ts @@ -4,38 +4,46 @@ import { isVertical, } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" -export const simplifyPath = (path: Point[]): Point[] => { - if (path.length < 3) return path - const newPath: Point[] = [path[0]] - for (let i = 1; i < path.length - 1; i++) { - const p1 = newPath[newPath.length - 1] - const p2 = path[i] - const p3 = path[i + 1] - if ( - (isVertical(p1, p2) && isVertical(p2, p3)) || - (isHorizontal(p1, p2) && isHorizontal(p2, p3)) - ) { +const EPS = 1e-9 + +const removeDuplicateConsecutivePoints = (path: Point[]): Point[] => { + if (path.length <= 1) return path + const result: Point[] = [path[0]] + for (let i = 1; i < path.length; i++) { + const prev = result[result.length - 1] + const cur = path[i] + if (Math.abs(prev.x - cur.x) < EPS && Math.abs(prev.y - cur.y) < EPS) { continue } - newPath.push(p2) + result.push(cur) } - newPath.push(path[path.length - 1]) + return result +} - if (newPath.length < 3) return newPath - const finalPath: Point[] = [newPath[0]] - for (let i = 1; i < newPath.length - 1; i++) { - const p1 = finalPath[finalPath.length - 1] - const p2 = newPath[i] - const p3 = newPath[i + 1] +const collapseCollinearPoints = (path: Point[]): Point[] => { + if (path.length < 3) return path + const result: Point[] = [path[0]] + for (let i = 1; i < path.length - 1; i++) { + const p1 = result[result.length - 1] + const p2 = path[i] + const p3 = path[i + 1] if ( (isVertical(p1, p2) && isVertical(p2, p3)) || (isHorizontal(p1, p2) && isHorizontal(p2, p3)) ) { continue } - finalPath.push(p2) + result.push(p2) } - finalPath.push(newPath[newPath.length - 1]) + result.push(path[path.length - 1]) + return result +} - return finalPath +export const simplifyPath = (path: Point[]): Point[] => { + let current = removeDuplicateConsecutivePoints(path) + current = collapseCollinearPoints(current) + current = removeDuplicateConsecutivePoints(current) + current = collapseCollinearPoints(current) + current = removeDuplicateConsecutivePoints(current) + return current } diff --git a/tests/examples/__snapshots__/example35.snap.svg b/tests/examples/__snapshots__/example35.snap.svg index c28367b23..f7507774e 100644 --- a/tests/examples/__snapshots__/example35.snap.svg +++ b/tests/examples/__snapshots__/example35.snap.svg @@ -2,120 +2,85 @@ +x-" data-x="-1.05" data-y="0.30000000000000004" cx="40" cy="415.609756097561" r="3" fill="hsl(319, 100%, 50%, 0.8)" /> +x-" data-x="-1.05" data-y="0.10000000000000003" cx="40" cy="442.9268292682927" r="3" fill="hsl(320, 100%, 50%, 0.8)" /> +x-" data-x="-1.05" data-y="-0.09999999999999998" cx="40" cy="470.2439024390244" r="3" fill="hsl(321, 100%, 50%, 0.8)" /> +x-" data-x="-1.05" data-y="-0.30000000000000004" cx="40" cy="497.56097560975616" r="3" fill="hsl(322, 100%, 50%, 0.8)" /> +x+" data-x="1.05" data-y="-0.30000000000000004" cx="326.82926829268297" cy="497.56097560975616" r="3" fill="hsl(323, 100%, 50%, 0.8)" /> +x+" data-x="1.05" data-y="-0.10000000000000003" cx="326.82926829268297" cy="470.24390243902445" r="3" fill="hsl(324, 100%, 50%, 0.8)" /> +x+" data-x="1.05" data-y="0.09999999999999998" cx="326.82926829268297" cy="442.92682926829275" r="3" fill="hsl(325, 100%, 50%, 0.8)" /> +x+" data-x="1.05" data-y="0.30000000000000004" cx="326.82926829268297" cy="415.609756097561" r="3" fill="hsl(326, 100%, 50%, 0.8)" /> +x-" data-x="0.95" data-y="2.3" cx="313.1707317073171" cy="142.43902439024396" r="3" fill="hsl(200, 100%, 50%, 0.8)" /> +x-" data-x="0.95" data-y="2.1" cx="313.1707317073171" cy="169.7560975609756" r="3" fill="hsl(201, 100%, 50%, 0.8)" /> +x-" data-x="0.95" data-y="1.9" cx="313.1707317073171" cy="197.07317073170736" r="3" fill="hsl(202, 100%, 50%, 0.8)" /> +x-" data-x="0.95" data-y="1.7" cx="313.1707317073171" cy="224.39024390243907" r="3" fill="hsl(203, 100%, 50%, 0.8)" /> +x+" data-x="3.05" data-y="1.7" cx="600" cy="224.39024390243907" r="3" fill="hsl(204, 100%, 50%, 0.8)" /> +x+" data-x="3.05" data-y="1.9" cx="600" cy="197.07317073170736" r="3" fill="hsl(205, 100%, 50%, 0.8)" /> +x+" data-x="3.05" data-y="2.1" cx="600" cy="169.7560975609756" r="3" fill="hsl(206, 100%, 50%, 0.8)" /> +x+" data-x="3.05" data-y="2.3" cx="600" cy="142.43902439024396" r="3" fill="hsl(207, 100%, 50%, 0.8)" /> - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +