Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/solvers/TraceCleanupSolver/simplifyPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,31 @@ import {
isVertical,
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"

/**
* Removes consecutive duplicate points (zero-length segments) from a path.
*
* These arise when rerouted segments are spliced together at the same
* coordinate — e.g. when _applyBestRoute concatenates path slices and the
* junction point appears twice. Zero-length segments render as tiny artifacts
* in the schematic output.
*/
export const removeDuplicateConsecutivePoints = (path: Point[]): Point[] => {
if (path.length === 0) return path
const result: Point[] = [path[0]]
for (let i = 1; i < path.length; i++) {
const prev = result[result.length - 1]
if (prev.x !== path[i].x || prev.y !== path[i].y) {
result.push(path[i])
}
}
return result
}

export const simplifyPath = (path: Point[]): Point[] => {
// Remove duplicate consecutive points (zero-length segments) first so the
// collinear-merge pass below doesn't see misleading direction changes.
path = removeDuplicateConsecutivePoints(path)

if (path.length < 3) return path
const newPath: Point[] = [path[0]]
for (let i = 1; i < path.length - 1; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { visualizeTightRectangle } from "../visualizeTightRectangle"
import { visualizeCandidates } from "./visualizeCandidates"
import { mergeGraphicsObjects } from "../mergeGraphicsObjects"
import { visualizeCollision } from "./visualizeCollision"
import { removeDuplicateConsecutivePoints } from "../simplifyPath"

/**
* Defines the input structure for the UntangleTraceSubsolver.
Expand Down Expand Up @@ -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 = [
// Splice the rerouted segment into the trace path. The slice boundaries
// may share a coordinate with the first/last point of bestRoute, so we
// remove any duplicate consecutive points that arise at the junctions.
const newTracePath = removeDuplicateConsecutivePoints([
...originalTrace.tracePath.slice(0, p2Index),
...bestRoute,
...originalTrace.tracePath.slice(p2Index + 1),
]
])
this.input.allTraces[traceIndex] = {
...originalTrace,
tracePath: newTracePath,
Expand Down
101 changes: 101 additions & 0 deletions tests/functions/simplifyPath.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, test } from "bun:test"
import {
removeDuplicateConsecutivePoints,
simplifyPath,
} from "lib/solvers/TraceCleanupSolver/simplifyPath"

describe("removeDuplicateConsecutivePoints", () => {
test("removes consecutive duplicates", () => {
const path = [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 0 }, // duplicate
{ x: 2, y: 0 },
]
const result = removeDuplicateConsecutivePoints(path)
expect(result).toEqual([
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
])
})

test("keeps non-consecutive duplicates", () => {
const path = [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 0, y: 0 }, // same as first but not consecutive
]
const result = removeDuplicateConsecutivePoints(path)
expect(result).toEqual(path)
})

test("handles empty path", () => {
expect(removeDuplicateConsecutivePoints([])).toEqual([])
})

test("handles single point", () => {
const path = [{ x: 1, y: 2 }]
expect(removeDuplicateConsecutivePoints(path)).toEqual(path)
})

test("removes multiple consecutive duplicates", () => {
const path = [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
]
const result = removeDuplicateConsecutivePoints(path)
expect(result).toEqual([
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
])
})
})

describe("simplifyPath", () => {
test("removes collinear points on horizontal segment", () => {
const path = [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
]
const result = simplifyPath(path)
expect(result).toEqual([
{ x: 0, y: 0 },
{ x: 2, y: 0 },
])
})

test("removes duplicate junction point introduced by _applyBestRoute splice", () => {
// When _applyBestRoute splices a segment, the endpoint of the left slice
// can equal the first point of bestRoute, producing a zero-length segment.
const path = [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 0 }, // duplicate at splice junction
{ x: 1, y: 1 },
{ x: 2, y: 1 },
]
const result = simplifyPath(path)
// Duplicate removed, path length should be smaller than input
expect(result.length).toBeLessThan(path.length)
// No consecutive duplicates in the output
for (let i = 1; i < result.length; i++) {
expect(
result[i].x !== result[i - 1].x || result[i].y !== result[i - 1].y,
).toBe(true)
}
})

test("path shorter than 3 points is returned as-is", () => {
const path = [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]
expect(simplifyPath(path)).toEqual(path)
})
})
Loading