diff --git a/lib/solvers/TraceCleanupSolver/simplifyPath.ts b/lib/solvers/TraceCleanupSolver/simplifyPath.ts
index e17bfb5..7c5d166 100644
--- a/lib/solvers/TraceCleanupSolver/simplifyPath.ts
+++ b/lib/solvers/TraceCleanupSolver/simplifyPath.ts
@@ -4,13 +4,33 @@ import {
isVertical,
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
+const EPS = 1e-9
+
+const isSamePoint = (a: Point, b: Point): boolean =>
+ Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS
+
+/**
+ * Removes duplicate consecutive points from a path.
+ */
+const removeDuplicateConsecutivePoints = (path: Point[]): Point[] => {
+ if (path.length < 2) return path
+ const result: Point[] = [path[0]]
+ for (let i = 1; i < path.length; i++) {
+ if (!isSamePoint(result[result.length - 1], path[i])) {
+ result.push(path[i])
+ }
+ }
+ return result
+}
+
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 dedupedPath = removeDuplicateConsecutivePoints(path)
+ if (dedupedPath.length < 3) return dedupedPath
+ const newPath: Point[] = [dedupedPath[0]]
+ for (let i = 1; i < dedupedPath.length - 1; i++) {
const p1 = newPath[newPath.length - 1]
- const p2 = path[i]
- const p3 = path[i + 1]
+ const p2 = dedupedPath[i]
+ const p3 = dedupedPath[i + 1]
if (
(isVertical(p1, p2) && isVertical(p2, p3)) ||
(isHorizontal(p1, p2) && isHorizontal(p2, p3))
@@ -19,7 +39,7 @@ export const simplifyPath = (path: Point[]): Point[] => {
}
newPath.push(p2)
}
- newPath.push(path[path.length - 1])
+ newPath.push(dedupedPath[dedupedPath.length - 1])
if (newPath.length < 3) return newPath
const finalPath: Point[] = [newPath[0]]
diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts
index 3519aa9..060ab82 100644
--- a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts
+++ b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts
@@ -23,6 +23,7 @@ import { visualizeTightRectangle } from "../visualizeTightRectangle"
import { visualizeCandidates } from "./visualizeCandidates"
import { mergeGraphicsObjects } from "../mergeGraphicsObjects"
import { visualizeCollision } from "./visualizeCollision"
+import { simplifyPath } from "../simplifyPath"
/**
* Defines the input structure for the UntangleTraceSubsolver.
@@ -258,11 +259,14 @@ export class UntangleTraceSubsolver extends BaseSolver {
p.x === this.currentLShape!.p2.x && p.y === this.currentLShape!.p2.y,
)
if (p2Index !== -1) {
- const newTracePath = [
+ const rawTracePath = [
...originalTrace.tracePath.slice(0, p2Index),
...bestRoute,
...originalTrace.tracePath.slice(p2Index + 1),
]
+ // Simplify to remove redundant collinear points and duplicates
+ // introduced by the path concatenation
+ const newTracePath = simplifyPath(rawTracePath)
this.input.allTraces[traceIndex] = {
...originalTrace,
tracePath: newTracePath,
diff --git a/tests/examples/__snapshots__/example29.snap.svg b/tests/examples/__snapshots__/example29.snap.svg
index c931ee3..77f66c3 100644
--- a/tests/examples/__snapshots__/example29.snap.svg
+++ b/tests/examples/__snapshots__/example29.snap.svg
@@ -516,7 +516,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r
-
+
@@ -831,7 +831,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r
-
+
@@ -849,7 +849,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r
-
+
@@ -1038,7 +1038,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r
-
+
diff --git a/tests/functions/simplifyPath.test.ts b/tests/functions/simplifyPath.test.ts
new file mode 100644
index 0000000..afc3f97
--- /dev/null
+++ b/tests/functions/simplifyPath.test.ts
@@ -0,0 +1,104 @@
+import { expect, test, describe } from "bun:test"
+import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
+
+describe("simplifyPath", () => {
+ test("should return path unchanged if less than 3 points", () => {
+ expect(simplifyPath([])).toEqual([])
+ expect(simplifyPath([{ x: 0, y: 0 }])).toEqual([{ x: 0, y: 0 }])
+ expect(
+ simplifyPath([
+ { x: 0, y: 0 },
+ { x: 1, y: 1 },
+ ]),
+ ).toEqual([
+ { x: 0, y: 0 },
+ { x: 1, y: 1 },
+ ])
+ })
+
+ test("should remove collinear horizontal points", () => {
+ const path = [
+ { x: 0, y: 0 },
+ { x: 1, y: 0 },
+ { x: 2, y: 0 },
+ { x: 3, y: 0 },
+ ]
+ expect(simplifyPath(path)).toEqual([
+ { x: 0, y: 0 },
+ { x: 3, y: 0 },
+ ])
+ })
+
+ test("should remove collinear vertical points", () => {
+ const path = [
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 0, y: 2 },
+ { x: 0, y: 3 },
+ ]
+ expect(simplifyPath(path)).toEqual([
+ { x: 0, y: 0 },
+ { x: 0, y: 3 },
+ ])
+ })
+
+ test("should preserve L-shaped corners", () => {
+ const path = [
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 1, y: 1 },
+ ]
+ expect(simplifyPath(path)).toEqual([
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 1, y: 1 },
+ ])
+ })
+
+ test("should remove duplicate consecutive points", () => {
+ const path = [
+ { x: 0, y: 0 },
+ { x: 0, y: 0 },
+ { x: 1, y: 0 },
+ { x: 1, y: 0 },
+ { x: 1, y: 1 },
+ ]
+ expect(simplifyPath(path)).toEqual([
+ { x: 0, y: 0 },
+ { x: 1, y: 0 },
+ { x: 1, y: 1 },
+ ])
+ })
+
+ test("should remove near-duplicate points within epsilon", () => {
+ const path = [
+ { x: 0, y: 0 },
+ { x: 1e-10, y: 0 },
+ { x: 1, y: 0 },
+ { x: 1, y: 1 },
+ ]
+ expect(simplifyPath(path)).toEqual([
+ { x: 0, y: 0 },
+ { x: 1, y: 0 },
+ { x: 1, y: 1 },
+ ])
+ })
+
+ test("should handle path with mixed collinear segments and corners", () => {
+ // Simulates the kind of path produced by UntangleTraceSubsolver rerouting
+ const path = [
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 0, y: 2 }, // collinear with prev two
+ { x: 1, y: 2 }, // corner
+ { x: 2, y: 2 }, // collinear
+ { x: 2, y: 3 }, // corner
+ ]
+ expect(simplifyPath(path)).toEqual([
+ { x: 0, y: 0 },
+ { x: 0, y: 2 },
+ { x: 2, y: 2 },
+ { x: 2, y: 3 },
+ ])
+ })
+})