diff --git a/lib/solvers/TraceCleanupSolver/simplifyPath.ts b/lib/solvers/TraceCleanupSolver/simplifyPath.ts index e17bfb52..b9becd54 100644 --- a/lib/solvers/TraceCleanupSolver/simplifyPath.ts +++ b/lib/solvers/TraceCleanupSolver/simplifyPath.ts @@ -4,38 +4,30 @@ import { isVertical, } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" -export const simplifyPath = (path: Point[]): Point[] => { +const dedupConsecutive = (path: Point[]): Point[] => + path.filter( + (p, i) => i === 0 || p.x !== path[i - 1]!.x || p.y !== path[i - 1]!.y, + ) + +const collapseCollinear = (path: Point[]): Point[] => { if (path.length < 3) return path - const newPath: Point[] = [path[0]] + const out: 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)) - ) { - continue - } - newPath.push(p2) - } - newPath.push(path[path.length - 1]) - - 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] - if ( - (isVertical(p1, p2) && isVertical(p2, p3)) || - (isHorizontal(p1, p2) && isHorizontal(p2, p3)) - ) { - continue - } - finalPath.push(p2) + const prev = out[out.length - 1]! + const cur = path[i]! + const next = path[i + 1]! + const collinear = + (isVertical(prev, cur) && isVertical(cur, next)) || + (isHorizontal(prev, cur) && isHorizontal(cur, next)) + if (!collinear) out.push(cur) } - finalPath.push(newPath[newPath.length - 1]) + out.push(path[path.length - 1]!) + return out +} - return finalPath +export const simplifyPath = (path: Point[]): Point[] => { + // Remove zero-length segments before each collinear collapse pass + const pass1 = collapseCollinear(dedupConsecutive(path)) + const pass2 = collapseCollinear(dedupConsecutive(pass1)) + return dedupConsecutive(pass2) } diff --git a/tests/solvers/TraceCleanupSolver/simplify-path.test.ts b/tests/solvers/TraceCleanupSolver/simplify-path.test.ts new file mode 100644 index 00000000..8ad1c2b5 --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/simplify-path.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test" +import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" + +test("removes duplicate consecutive points (zero-length segments)", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 0 }, // duplicate + { x: 2, y: 0 }, + ] + const result = simplifyPath(path) + // Collinear + dedup → single segment A→D + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ x: 0, y: 0 }) + expect(result[result.length - 1]).toEqual({ x: 2, y: 0 }) +}) + +test("collapses collinear points on a straight horizontal path", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 3, y: 0 }, + ] + const result = simplifyPath(path) + expect(result).toHaveLength(2) +}) + +test("preserves a right-angle turn", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + ] + const result = simplifyPath(path) + expect(result).toHaveLength(3) +}) + +test("handles duplicate at start and end without crashing", () => { + const path = [ + { x: 0, y: 0 }, + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 0 }, + ] + const result = simplifyPath(path) + expect(result[0]).toEqual({ x: 0, y: 0 }) + expect(result[result.length - 1]).toEqual({ x: 1, y: 0 }) +}) + +test("idempotent: simplifying twice gives the same result", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 2, y: 1 }, + { x: 3, y: 1 }, + ] + const once = simplifyPath(path) + const twice = simplifyPath(once) + expect(twice).toEqual(once) +})