diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index da39fd852..c25934a79 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -1,18 +1,20 @@ -import type { InputProblem } from "lib/types/InputProblem" import type { GraphicsObject, Line } from "graphics-debug" -import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels" -import { balanceZShapes } from "./balanceZShapes" import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" +import type { InputProblem } from "lib/types/InputProblem" import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" import { alignSameNetRails } from "./alignSameNetRails" +import { balanceZShapes } from "./balanceZShapes" +import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels" +import { straightenNearOrthogonalSegments } from "./straightenNearOrthogonalSegments" export type TraceCleanupOperation = | "untangling_traces" | "minimizing_turns" | "balancing_l_shapes" | "aligning_same_net_rails" + | "straightening_near_orthogonal_segments" /** * Defines the input structure for the TraceCleanupSolver. @@ -27,8 +29,8 @@ export interface TraceCleanupSolverInput { eligibleTraceIds?: ReadonlySet } -import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" import { is4PointRectangle } from "./is4PointRectangle" +import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" /** * Represents the different stages or steps within the trace cleanup pipeline. @@ -37,6 +39,7 @@ const DEFAULT_OPERATIONS: readonly TraceCleanupOperation[] = [ "untangling_traces", "minimizing_turns", "balancing_l_shapes", + "straightening_near_orthogonal_segments", ] /** @@ -106,6 +109,9 @@ export class TraceCleanupSolver extends BaseSolver { case "aligning_same_net_rails": this._runAlignSameNetRailsStep() break + case "straightening_near_orthogonal_segments": + this._runStraightenNearOrthogonalSegmentsStep() + break } } @@ -188,6 +194,14 @@ export class TraceCleanupSolver extends BaseSolver { this._advancePipeline() } + private _runStraightenNearOrthogonalSegmentsStep() { + const straightened = straightenNearOrthogonalSegments(this.outputTraces) + this.outputTraces = straightened.traces + this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) + this.stats.straightenedSegmentCount = straightened.straightenedSegmentCount + this._advancePipeline() + } + getOutput() { return { traces: this.outputTraces, diff --git a/lib/solvers/TraceCleanupSolver/straightenNearOrthogonalSegments.ts b/lib/solvers/TraceCleanupSolver/straightenNearOrthogonalSegments.ts new file mode 100644 index 000000000..b560cef50 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/straightenNearOrthogonalSegments.ts @@ -0,0 +1,59 @@ +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +/** + * Largest deviation, in schematic units, that is treated as a segment meant to + * be orthogonal. Pin coordinates in real inputs are occasionally off by a + * fraction of a mil (observed: `5.3999378` against a neighbouring `5.4`), which + * leaves a segment a few hundredths of a percent off axis. That renders as a + * visibly skewed trace even though every other segment is orthogonal. + * + * Kept well below any intentional offset — the smallest deliberate jog in the + * solvers is `LABEL_SEARCH_STEP` at 0.1 — so a real diagonal is never + * flattened. + */ +const MAX_SNAP_DEVIATION = 1e-3 + +/** + * Snaps segments that are within `MAX_SNAP_DEVIATION` of horizontal or vertical + * onto the axis exactly. + * + * Schematic traces are orthogonal by construction, so a segment that is almost + * but not quite axis-aligned is numeric noise inherited from the input rather + * than a routing decision. + */ +export const straightenNearOrthogonalSegments = ( + traces: SolvedTracePath[], +): { traces: SolvedTracePath[]; straightenedSegmentCount: number } => { + let straightenedSegmentCount = 0 + + const outputTraces = traces.map((trace) => { + const path = trace.tracePath.map((point) => ({ ...point })) + let changed = false + + for (let i = 0; i < path.length - 1; i++) { + const a = path[i]! + const b = path[i + 1]! + const dx = Math.abs(a.x - b.x) + const dy = Math.abs(a.y - b.y) + + // Already axis-aligned, or a genuine diagonal — leave both alone. + if (dx === 0 || dy === 0) continue + if (dx > MAX_SNAP_DEVIATION && dy > MAX_SNAP_DEVIATION) continue + + // Collapse the smaller deviation. Moving the later point keeps the + // trace's starting pin exactly where the input put it. + if (dx < dy) { + b.x = a.x + } else { + b.y = a.y + } + + changed = true + straightenedSegmentCount++ + } + + return changed ? { ...trace, tracePath: path } : trace + }) + + return { traces: outputTraces, straightenedSegmentCount } +} diff --git a/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg b/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg index 7a6bb6490..38464c21a 100644 --- a/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +++ b/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg @@ -1,10 +1,10 @@ -