diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..0eb874b68 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -6,6 +6,7 @@ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { mergeSameNetCloseTraceLines } from "./mergeSameNetCloseTraceLines" /** * Defines the input structure for the TraceCleanupSolver. @@ -27,6 +28,7 @@ import { is4PointRectangle } from "./is4PointRectangle" type PipelineStep = | "minimizing_turns" | "balancing_l_shapes" + | "merging_same_net_close_lines" | "untangling_traces" /** @@ -84,6 +86,9 @@ export class TraceCleanupSolver extends BaseSolver { case "balancing_l_shapes": this._runBalanceLShapesStep() break + case "merging_same_net_close_lines": + this._runMergeSameNetCloseLinesStep() + break } } @@ -108,13 +113,21 @@ export class TraceCleanupSolver extends BaseSolver { private _runBalanceLShapesStep() { if (this.traceIdQueue.length === 0) { - this.solved = true + this.pipelineStep = "merging_same_net_close_lines" return } this._processTrace("balancing_l_shapes") } + private _runMergeSameNetCloseLinesStep() { + this.outputTraces = mergeSameNetCloseTraceLines( + Array.from(this.tracesMap.values()), + ) + this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) + this.solved = true + } + private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") { const targetMspConnectionPairId = this.traceIdQueue.shift()! this.activeTraceId = targetMspConnectionPairId diff --git a/lib/solvers/TraceCleanupSolver/mergeSameNetCloseTraceLines.ts b/lib/solvers/TraceCleanupSolver/mergeSameNetCloseTraceLines.ts new file mode 100644 index 000000000..0167e03f0 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/mergeSameNetCloseTraceLines.ts @@ -0,0 +1,218 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { simplifyPath } from "./simplifyPath" + +type SegmentOrientation = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + segmentIndex: number + orientation: SegmentOrientation + axisCoord: number + rangeMin: number + rangeMax: number + length: number +} + +export interface MergeSameNetCloseTraceLinesOptions { + closeThreshold?: number + eps?: number + maxIterations?: number +} + +const DEFAULT_CLOSE_THRESHOLD = 0.3 +const DEFAULT_EPS = 1e-6 +const DEFAULT_MAX_ITERATIONS = 1000 + +const pointsEqual = (a: Point, b: Point, eps: number) => + Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps + +const clonePoint = (p: Point): Point => ({ x: p.x, y: p.y }) + +const cloneTraces = (traces: SolvedTracePath[]): SolvedTracePath[] => + traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map(clonePoint), + })) + +const getSegmentsForTrace = ( + trace: SolvedTracePath, + traceIndex: number, + eps: number, +): SegmentRef[] => { + const segments: SegmentRef[] = [] + + for (let i = 0; i < trace.tracePath.length - 1; i++) { + const p1 = trace.tracePath[i]! + const p2 = trace.tracePath[i + 1]! + const dx = Math.abs(p1.x - p2.x) + const dy = Math.abs(p1.y - p2.y) + + if (dx <= eps && dy <= eps) continue + + if (dy <= eps) { + segments.push({ + traceIndex, + segmentIndex: i, + orientation: "horizontal", + axisCoord: (p1.y + p2.y) / 2, + rangeMin: Math.min(p1.x, p2.x), + rangeMax: Math.max(p1.x, p2.x), + length: dx, + }) + } else if (dx <= eps) { + segments.push({ + traceIndex, + segmentIndex: i, + orientation: "vertical", + axisCoord: (p1.x + p2.x) / 2, + rangeMin: Math.min(p1.y, p2.y), + rangeMax: Math.max(p1.y, p2.y), + length: dy, + }) + } + } + + return segments +} + +const projectedRangesOverlap = (a: SegmentRef, b: SegmentRef, eps: number) => + Math.min(a.rangeMax, b.rangeMax) - Math.max(a.rangeMin, b.rangeMin) > eps + +const chooseAnchor = (a: SegmentRef, b: SegmentRef): SegmentRef => { + if (a.length !== b.length) return a.length > b.length ? a : b + if (a.traceIndex !== b.traceIndex) return a.traceIndex < b.traceIndex ? a : b + return a.segmentIndex <= b.segmentIndex ? a : b +} + +const setSegmentAxisCoord = ( + trace: SolvedTracePath, + segmentIndex: number, + orientation: SegmentOrientation, + axisCoord: number, +) => { + const p1 = trace.tracePath[segmentIndex]! + const p2 = trace.tracePath[segmentIndex + 1]! + + if (orientation === "horizontal") { + p1.y = axisCoord + p2.y = axisCoord + } else { + p1.x = axisCoord + p2.x = axisCoord + } +} + +const removeConsecutiveDuplicatePoints = (path: Point[], eps: number) => { + const deduped: Point[] = [] + for (const point of path) { + if ( + !deduped.length || + !pointsEqual(deduped[deduped.length - 1]!, point, eps) + ) { + deduped.push(point) + } + } + return deduped +} + +const restoreOriginalEndpoints = ( + trace: SolvedTracePath, + originalTrace: SolvedTracePath, + eps: number, +) => { + const path = trace.tracePath + if (path.length < 2) return + + const originalStart = originalTrace.tracePath[0]! + const originalEnd = + originalTrace.tracePath[originalTrace.tracePath.length - 1]! + const shiftedStart = path[0]! + const shiftedEnd = path[path.length - 1]! + + let restoredPath = path + + if (!pointsEqual(shiftedStart, originalStart, eps)) { + restoredPath = [ + clonePoint(originalStart), + clonePoint(shiftedStart), + ...restoredPath.slice(1), + ] + } + + if (!pointsEqual(shiftedEnd, originalEnd, eps)) { + restoredPath = [ + ...restoredPath.slice(0, -1), + clonePoint(shiftedEnd), + clonePoint(originalEnd), + ] + } + + trace.tracePath = simplifyPath( + removeConsecutiveDuplicatePoints(restoredPath, eps), + ) +} + +export const mergeSameNetCloseTraceLines = ( + traces: SolvedTracePath[], + opts: MergeSameNetCloseTraceLinesOptions = {}, +): SolvedTracePath[] => { + const closeThreshold = opts.closeThreshold ?? DEFAULT_CLOSE_THRESHOLD + const eps = opts.eps ?? DEFAULT_EPS + const maxIterations = opts.maxIterations ?? DEFAULT_MAX_ITERATIONS + const outputTraces = cloneTraces(traces) + const originalTraces = cloneTraces(traces) + + for (let iteration = 0; iteration < maxIterations; iteration++) { + let mergedThisIteration = false + + const segmentsByNet = new Map() + for (let traceIndex = 0; traceIndex < outputTraces.length; traceIndex++) { + const trace = outputTraces[traceIndex]! + const segments = getSegmentsForTrace(trace, traceIndex, eps) + const existing = segmentsByNet.get(trace.globalConnNetId) ?? [] + existing.push(...segments) + segmentsByNet.set(trace.globalConnNetId, existing) + } + + for (const segments of segmentsByNet.values()) { + for (let i = 0; i < segments.length && !mergedThisIteration; i++) { + for (let j = i + 1; j < segments.length; j++) { + const a = segments[i]! + const b = segments[j]! + + if (a.traceIndex === b.traceIndex) continue + if (a.orientation !== b.orientation) continue + if (Math.abs(a.axisCoord - b.axisCoord) > closeThreshold) continue + if (!projectedRangesOverlap(a, b, eps)) continue + + const anchor = chooseAnchor(a, b) + const target = anchor === a ? b : a + + if (Math.abs(target.axisCoord - anchor.axisCoord) <= eps) continue + + setSegmentAxisCoord( + outputTraces[target.traceIndex]!, + target.segmentIndex, + target.orientation, + anchor.axisCoord, + ) + mergedThisIteration = true + break + } + } + if (mergedThisIteration) break + } + + if (!mergedThisIteration) break + } + + for (let i = 0; i < outputTraces.length; i++) { + restoreOriginalEndpoints(outputTraces[i]!, originalTraces[i]!, eps) + outputTraces[i]!.tracePath = simplifyPath( + removeConsecutiveDuplicatePoints(outputTraces[i]!.tracePath, eps), + ) + } + + return outputTraces +} diff --git a/tests/examples/__snapshots__/example14.snap.svg b/tests/examples/__snapshots__/example14.snap.svg index b91ea230a..39785b8e1 100644 --- a/tests/examples/__snapshots__/example14.snap.svg +++ b/tests/examples/__snapshots__/example14.snap.svg @@ -73,36 +73,28 @@ y-" data-x="1.2000000000000002" data-y="1.1500000000000001" cx="419.076923076923 y+" data-x="1.2000000000000002" data-y="2.25" cx="419.07692307692315" cy="98.15384615384613" r="3" fill="hsl(349, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + @@ -150,7 +142,7 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 - + @@ -196,43 +188,35 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 +globalConnNetId: connectivity_net4" data-x="1.3510000000000002" data-y="-0.5760000000000001" x="423.4707692307693" y="322.24" width="17.230769230769226" height="38.769230769230774" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net4" data-x="-1.2000000000000002" data-y="-2.6750000000000003" x="203.6923076923077" y="503.0769230769231" width="17.230769230769255" height="38.76923076923083" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net1" data-x="-1.6250000000000002" data-y="-0.30000000000000004" x="156.30769230769232" y="309.2307692307692" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net3" data-x="1.6250000000000002" data-y="0.09999999999999998" x="436.3076923076924" y="274.7692307692308" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net5" data-x="-1.3010000000000002" data-y="0.7260000000000001" x="194.99076923076925" y="210.0676923076923" width="17.230769230769255" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net5" data-x="3.2" data-y="0.525" x="582.7692307692308" y="227.3846153846154" width="17.230769230769283" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net0" data-x="-1.5500000000000003" data-y="0.32500000000000007" x="173.53846153846155" y="244.6153846153846" width="17.230769230769226" height="38.769230769230745" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net2" data-x="1.4755000000000003" data-y="-1.0694553500000001" x="434.19692307692316" y="364.7530763076923" width="17.230769230769226" height="38.769230769230774" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> - + - + - + - + - + - + - + - + - + @@ -794,10 +785,10 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 - + - + @@ -882,48 +873,39 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 +globalConnNetId: connectivity_net0" data-x="-1.3099999999999998" data-y="1.4250000000000016" x="348.85072655217965" y="311.4927344782034" width="9.863496257155475" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net0" data-x="0.29250000000000076" data-y="6.9300000000000015" x="427.8819903126376" y="40.00000000000006" width="9.863496257155475" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net6" data-x="-1.3099999999999998" data-y="3.0250000000000017" x="348.85072655217965" y="232.58476442095986" width="9.863496257155475" height="22.192866578599762" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net6" data-x="-3.7550000000000003" data-y="4.193333333333335" x="228.26948480845445" y="174.96550711874357" width="9.863496257155447" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net9" data-x="-2.25" data-y="-1.4000000000000004" x="296.327608982827" y="456.97930427124606" width="22.192866578599705" height="9.863496257155475" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net8" data-x="-2.25" data-y="2.200000000000001" x="296.327608982827" y="279.4363716424482" width="22.192866578599705" height="9.863496257155475" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net10" data-x="0.29250000000000076" data-y="4.980000000000002" x="427.8819903126376" y="136.16908850726549" width="9.863496257155475" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net10" data-x="-3.7550000000000003" data-y="2.2433333333333345" x="228.26948480845445" y="271.13459562600906" width="9.863496257155447" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" /> +globalConnNetId: connectivity_net10" data-x="-2.49" data-y="-3.1250000000000004" x="290.6560986349626" y="535.8872743284896" width="9.863496257155418" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" />