diff --git a/lib/solvers/TraceCleanupSolver/simplifyPath.ts b/lib/solvers/TraceCleanupSolver/simplifyPath.ts index e17bfb52..e8d4467a 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 + +/** + * Remove consecutive duplicate points from a path. + * Two points are considered duplicates if they are within EPS distance. + */ +export const removeDuplicateConsecutivePoints = (path: Point[]): Point[] => { + if (path.length < 2) return path + const result: Point[] = [path[0]] + for (let i = 1; i < path.length; i++) { + const prev = result[result.length - 1] + const curr = path[i] + if (Math.abs(prev.x - curr.x) > EPS || Math.abs(prev.y - curr.y) > EPS) { + result.push(curr) + } + } + 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 deduped = removeDuplicateConsecutivePoints(path) + if (deduped.length < 3) return deduped + const newPath: Point[] = [deduped[0]] + for (let i = 1; i < deduped.length - 1; i++) { const p1 = newPath[newPath.length - 1] - const p2 = path[i] - const p3 = path[i + 1] + const p2 = deduped[i] + const p3 = deduped[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(deduped[deduped.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 3519aa90..095908d8 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts @@ -8,6 +8,7 @@ import { getTraceObstacles } from "./getTraceObstacles" import { findIntersectionsWithObstacles } from "./findIntersectionsWithObstacles" import { generateLShapeRerouteCandidates } from "./generateLShapeRerouteCandidates" import { isPathColliding, type CollisionInfo } from "./isPathColliding" +import { removeDuplicateConsecutivePoints } from "../simplifyPath" import { generateRectangleCandidates, type Rectangle, @@ -24,9 +25,6 @@ import { visualizeCandidates } from "./visualizeCandidates" import { mergeGraphicsObjects } from "../mergeGraphicsObjects" import { visualizeCollision } from "./visualizeCollision" -/** - * Defines the input structure for the UntangleTraceSubsolver. - */ export interface UntangleTraceSubsolverInput { inputProblem: InputProblem allTraces: SolvedTracePath[] @@ -35,26 +33,12 @@ export interface UntangleTraceSubsolverInput { paddingBuffer: number } -/** - * Represents the different visualization modes for the UntangleTraceSubsolver. - */ type VisualizationMode = | "l_shapes" | "intersection_points" | "tight_rectangle" | "candidates" -/** - * The UntangleTraceSubsolver is designed to resolve complex overlaps and improve the routing of traces, - * particularly focusing on "L-shaped" turns that might be causing congestion or suboptimal paths. - * Its main workflow involves several steps: - * 1. **Identify L-Shapes**: It first identifies all L-shaped turns within the traces that need processing. - * 2. **Find Intersections**: For each L-shape, it determines intersection points with other traces and obstacles. - * 3. **Generate Rectangle Candidates**: Based on these intersection points, it generates potential rectangular regions for rerouting. - * 4. **Evaluate Candidates**: For each rectangular candidate, it generates alternative trace paths and evaluates them for collisions. - * 5. **Apply Best Route**: If a collision-free and improved route is found, it updates the trace path. - * This iterative process aims to untangle traces and create a cleaner, more efficient layout. - */ export class UntangleTraceSubsolver extends BaseSolver { private input: UntangleTraceSubsolverInput private lShapesToProcess: LShape[] = [] @@ -131,8 +115,8 @@ export class UntangleTraceSubsolver extends BaseSolver { this.currentLShape = null this.currentCandidateIndex = 0 this.lShapeJustProcessed = false - this.visualizationMode = "l_shapes" // Reset visualization mode - this.intersectionPoints = [] // Clear temporary data + this.visualizationMode = "l_shapes" + this.intersectionPoints = [] this.tightRectangle = null this.candidates = [] this.bestRoute = null @@ -258,11 +242,11 @@ export class UntangleTraceSubsolver extends BaseSolver { p.x === this.currentLShape!.p2.x && p.y === this.currentLShape!.p2.y, ) if (p2Index !== -1) { - const newTracePath = [ + const newTracePath = removeDuplicateConsecutivePoints([ ...originalTrace.tracePath.slice(0, p2Index), ...bestRoute, ...originalTrace.tracePath.slice(p2Index + 1), - ] + ]) this.input.allTraces[traceIndex] = { ...originalTrace, tracePath: newTracePath, @@ -280,18 +264,6 @@ export class UntangleTraceSubsolver extends BaseSolver { } override visualize(): GraphicsObject { - // console.log("VISUALIZE STATE:", { - // step: this.lShapeProcessingStep, - // vizMode: this.visualizationMode, - // lShape: this.currentLShape?.traceId, - // rectIdx: this.currentRectangleIndex, - // rectCount: this.rectangleCandidates.length, - // tightRect: this.tightRectangle, - // pathIdx: this.currentCandidateIndex, - // pathCount: this.candidates.length, - // lastCollision: this.lastCollision?.isColliding, - // }) - switch (this.visualizationMode) { case "l_shapes": return visualizeLSapes(this.lShapesToProcess) @@ -329,7 +301,7 @@ export class UntangleTraceSubsolver extends BaseSolver { for (let i = 0; i < trace.tracePath.length - 1; i++) { allTracesGraphics.lines!.push({ points: [trace.tracePath[i], trace.tracePath[i + 1]], - strokeColor: "#ccc", // Light gray for other traces + strokeColor: "#ccc", }) } } diff --git a/site/examples/example32-fix-extra-trace-lines.page.tsx b/site/examples/example32-fix-extra-trace-lines.page.tsx new file mode 100644 index 00000000..94109d87 --- /dev/null +++ b/site/examples/example32-fix-extra-trace-lines.page.tsx @@ -0,0 +1,80 @@ +import type { InputProblem } from "lib/types/InputProblem" +import { PipelineDebugger } from "site/components/PipelineDebugger" + +/** + * Example reproducing extra trace lines from Issue #78. + * + * When UntangleTraceSubsolver reroutes L-shaped corners, the path + * concatenation can produce consecutive duplicate points, resulting + * in zero-length "extra" trace segments that render as visual artifacts. + * + * This circuit forces the untangle step to reroute by placing chips + * close together with crossing traces and a high maxMspPairDistance. + */ +export const inputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: 0, y: 0 }, + width: 2, + height: 1.2, + pins: [ + { pinId: "U1.1", x: -1, y: 0.4 }, + { pinId: "U1.2", x: -1, y: -0.4 }, + { pinId: "U1.3", x: 1, y: 0.4 }, + { pinId: "U1.4", x: 1, y: -0.4 }, + ], + }, + { + chipId: "R1", + center: { x: -3.5, y: 2 }, + width: 0.6, + height: 1, + pins: [ + { pinId: "R1.1", x: -3.5, y: 2.5 }, + { pinId: "R1.2", x: -3.5, y: 1.5 }, + ], + }, + { + chipId: "R2", + center: { x: 3.5, y: 2 }, + width: 0.6, + height: 1, + pins: [ + { pinId: "R2.1", x: 3.5, y: 2.5 }, + { pinId: "R2.2", x: 3.5, y: 1.5 }, + ], + }, + { + chipId: "C1", + center: { x: -3.5, y: -2 }, + width: 0.6, + height: 1, + pins: [ + { pinId: "C1.1", x: -3.5, y: -1.5 }, + { pinId: "C1.2", x: -3.5, y: -2.5 }, + ], + }, + { + chipId: "C2", + center: { x: 3.5, y: -2 }, + width: 0.6, + height: 1, + pins: [ + { pinId: "C2.1", x: 3.5, y: -1.5 }, + { pinId: "C2.2", x: 3.5, y: -2.5 }, + ], + }, + ], + directConnections: [ + { pinIds: ["U1.1", "R2.2"], netId: "NET1" }, + { pinIds: ["U1.3", "R1.2"], netId: "NET2" }, + { pinIds: ["U1.2", "C2.1"], netId: "NET3" }, + { pinIds: ["U1.4", "C1.1"], netId: "NET4" }, + ], + netConnections: [], + availableNetLabelOrientations: {}, + maxMspPairDistance: 6, +} + +export default () => diff --git a/tests/examples/example32.test.ts b/tests/examples/example32.test.ts new file mode 100644 index 00000000..9b771c8e --- /dev/null +++ b/tests/examples/example32.test.ts @@ -0,0 +1,23 @@ +import { test, expect } from "bun:test" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" +import { inputProblem } from "site/examples/example32-fix-extra-trace-lines.page" +import "tests/fixtures/matcher" + +/** + * Test for Issue #78: Fix extra trace lines in post-processing step. + * + * This circuit creates crossing traces that force the UntangleTraceSubsolver + * to reroute L-shaped corners. Before the fix, the rerouting produced + * consecutive duplicate points in trace paths, rendering as extra zero-length + * trace segments. + * + * After the fix, removeDuplicateConsecutivePoints in _applyBestRoute() + * eliminates these artifacts. + */ +test("example32: no extra trace lines from duplicate consecutive points", () => { + const solver = new SchematicTracePipelineSolver(inputProblem) + + solver.solve() + + expect(solver).toMatchSolverSnapshot(import.meta.path) +}) diff --git a/tests/unit/removeDuplicateConsecutivePoints.test.ts b/tests/unit/removeDuplicateConsecutivePoints.test.ts new file mode 100644 index 00000000..185a67b4 --- /dev/null +++ b/tests/unit/removeDuplicateConsecutivePoints.test.ts @@ -0,0 +1,102 @@ +import { test, expect, describe } from "bun:test" +import { + removeDuplicateConsecutivePoints, + simplifyPath, +} from "lib/solvers/TraceCleanupSolver/simplifyPath" + +describe("removeDuplicateConsecutivePoints", () => { + test("empty path returns empty", () => { + expect(removeDuplicateConsecutivePoints([])).toEqual([]) + }) + + test("single point returns same", () => { + expect(removeDuplicateConsecutivePoints([{ x: 1, y: 2 }])).toEqual([ + { x: 1, y: 2 }, + ]) + }) + + test("no duplicates returns same path", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + ] + expect(removeDuplicateConsecutivePoints(path)).toEqual(path) + }) + + test("removes consecutive duplicates", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + ] + expect(removeDuplicateConsecutivePoints(path)).toEqual([ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + ]) + }) + + test("removes multiple consecutive duplicates", () => { + const path = [ + { x: 0, y: 0 }, + { x: 0, y: 0 }, + { x: 0, y: 0 }, + { x: 1, y: 1 }, + { x: 1, y: 1 }, + ] + expect(removeDuplicateConsecutivePoints(path)).toEqual([ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ]) + }) + + test("removes near-duplicates within epsilon", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1 + 1e-12, y: 0 + 1e-12 }, + { x: 2, y: 0 }, + ] + expect(removeDuplicateConsecutivePoints(path)).toEqual([ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + ]) + }) +}) + +describe("simplifyPath with duplicates", () => { + test("simplifyPath handles consecutive duplicates gracefully", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 2, y: 1 }, + ] + const result = simplifyPath(path) + for (let i = 1; i < result.length; i++) { + const same = + Math.abs(result[i].x - result[i - 1].x) < 1e-9 && + Math.abs(result[i].y - result[i - 1].y) < 1e-9 + expect(same).toBe(false) + } + }) + + test("simplifyPath still removes collinear midpoints", () => { + const path = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 2, y: 0 }, + { x: 2, y: 1 }, + ] + const result = simplifyPath(path) + expect(result).toEqual([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 2, y: 1 }, + ]) + }) +})