From 37a50dc30262ce829aa9d0a8cae9f470e7963779 Mon Sep 17 00:00:00 2001 From: guo Date: Fri, 22 May 2026 17:14:55 +0800 Subject: [PATCH] Group nearby same-net trace segments before cleanup The cleanup solver now runs a bounded alignment phase after untangling so close interior segments on the same net share the longest nearby axis before later simplification steps run. The phase leaves terminal segments anchored and rejects moves that would add chip, label, or different-net trace intersections. Constraint: Algora bounty issues #29 and #34 ask for close same-net trace segments to be combined/aligned without breaking pin attachments. Rejected: Move endpoint segments too | would detach traces from pins and labels. Rejected: Treat same-net traces as obstacles | would prevent the intended overlap/coalescing behavior. Confidence: medium Scope-risk: moderate Directive: Keep same-net alignment before turn minimization so later cleanup can simplify the adjusted paths. Tested: npx bun test Tested: npm exec --package typescript@5.9.3 -- tsc --noEmit Tested: npm run build Not-tested: Browser demo video capture in the Vercel/Cosmos UI. --- .../TraceCleanupSolver/TraceCleanupSolver.ts | 34 +- .../alignSameNetTraceSegments.ts | 408 ++++++++++++++++++ .../examples/__snapshots__/example18.snap.svg | 32 +- .../examples/__snapshots__/example19.snap.svg | 14 +- .../align-same-net-trace-segments.test.ts | 183 ++++++++ 5 files changed, 635 insertions(+), 36 deletions(-) create mode 100644 lib/solvers/TraceCleanupSolver/alignSameNetTraceSegments.ts create mode 100644 tests/solvers/TraceCleanupSolver/align-same-net-trace-segments.test.ts diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..551ea88c8 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -1,11 +1,12 @@ -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 { alignSameNetTraceSegments } from "./alignSameNetTraceSegments" +import { balanceZShapes } from "./balanceZShapes" +import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels" /** * Defines the input structure for the TraceCleanupSolver. @@ -18,13 +19,14 @@ interface TraceCleanupSolverInput { paddingBuffer: number } -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. */ type PipelineStep = + | "aligning_same_net_segments" | "minimizing_turns" | "balancing_l_shapes" | "untangling_traces" @@ -66,10 +68,10 @@ export class TraceCleanupSolver extends BaseSolver { this.outputTraces = output.traces this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) this.activeSubSolver = null - this.pipelineStep = "minimizing_turns" + this.pipelineStep = "aligning_same_net_segments" } else if (this.activeSubSolver.failed) { this.activeSubSolver = null - this.pipelineStep = "minimizing_turns" + this.pipelineStep = "aligning_same_net_segments" } return } @@ -78,6 +80,9 @@ export class TraceCleanupSolver extends BaseSolver { case "untangling_traces": this._runUntangleTracesStep() break + case "aligning_same_net_segments": + this._runAlignSameNetSegmentsStep() + break case "minimizing_turns": this._runMinimizeTurnsStep() break @@ -94,6 +99,23 @@ export class TraceCleanupSolver extends BaseSolver { }) } + private _runAlignSameNetSegmentsStep() { + const result = alignSameNetTraceSegments({ + traces: Array.from(this.tracesMap.values()), + inputProblem: this.input.inputProblem, + allLabelPlacements: this.input.allLabelPlacements, + mergedLabelNetIdMap: this.input.mergedLabelNetIdMap, + paddingBuffer: this.input.paddingBuffer, + }) + + if (result.changed) { + this.outputTraces = result.traces + this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) + } + + this.pipelineStep = "minimizing_turns" + } + private _runMinimizeTurnsStep() { if (this.traceIdQueue.length === 0) { this.pipelineStep = "balancing_l_shapes" diff --git a/lib/solvers/TraceCleanupSolver/alignSameNetTraceSegments.ts b/lib/solvers/TraceCleanupSolver/alignSameNetTraceSegments.ts new file mode 100644 index 000000000..c7a136a11 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/alignSameNetTraceSegments.ts @@ -0,0 +1,408 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" +import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import type { InputProblem } from "lib/types/InputProblem" +import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { simplifyPath } from "./simplifyPath" + +type Orientation = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + segmentIndex: number + orientation: Orientation + fixedCoord: number + min: number + max: number + length: number + netId: string +} + +interface RectObstacle { + chipId: string + minX: number + minY: number + maxX: number + maxY: number +} + +interface AlignSameNetTraceSegmentsOptions { + inputProblem?: InputProblem + allLabelPlacements?: NetLabelPlacement[] + mergedLabelNetIdMap?: Record> + paddingBuffer?: number + maxAlignmentDistance?: number + maxEndpointGap?: number + maxIterations?: number +} + +interface AlignSameNetTraceSegmentsParams + extends AlignSameNetTraceSegmentsOptions { + traces: SolvedTracePath[] +} + +const EPS = 1e-6 +const TRACE_WIDTH = 0.01 +const STATIC_OBSTACLE_PADDING = 0.01 + +const cloneTrace = (trace: SolvedTracePath): SolvedTracePath => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), +}) + +const normalizePath = (path: Point[]): Point[] => { + const withoutDuplicatePoints: Point[] = [] + + for (const point of path) { + const previous = withoutDuplicatePoints[withoutDuplicatePoints.length - 1] + if ( + previous && + Math.abs(previous.x - point.x) < EPS && + Math.abs(previous.y - point.y) < EPS + ) { + continue + } + withoutDuplicatePoints.push(point) + } + + return simplifyPath(withoutDuplicatePoints) +} + +const collectInteriorSegments = ( + traces: SolvedTracePath[], + traceIndex: number, +): SegmentRef[] => { + const trace = traces[traceIndex]! + const segments: SegmentRef[] = [] + + for ( + let segmentIndex = 1; + segmentIndex < trace.tracePath.length - 2; + segmentIndex++ + ) { + const start = trace.tracePath[segmentIndex]! + const end = trace.tracePath[segmentIndex + 1]! + + if (Math.abs(start.y - end.y) < EPS) { + const min = Math.min(start.x, end.x) + const max = Math.max(start.x, end.x) + if (max - min > EPS) { + segments.push({ + traceIndex, + segmentIndex, + orientation: "horizontal", + fixedCoord: start.y, + min, + max, + length: max - min, + netId: trace.globalConnNetId, + }) + } + } else if (Math.abs(start.x - end.x) < EPS) { + const min = Math.min(start.y, end.y) + const max = Math.max(start.y, end.y) + if (max - min > EPS) { + segments.push({ + traceIndex, + segmentIndex, + orientation: "vertical", + fixedCoord: start.x, + min, + max, + length: max - min, + netId: trace.globalConnNetId, + }) + } + } + } + + return segments +} + +const areEquivalentNetIds = ( + leftNetId: string, + rightNetId: string, + mergedLabelNetIdMap: Record> = {}, +) => { + if (leftNetId === rightNetId) return true + return ( + mergedLabelNetIdMap[leftNetId]?.has(rightNetId) === true || + mergedLabelNetIdMap[rightNetId]?.has(leftNetId) === true + ) +} + +const rangesAreNear = ( + left: SegmentRef, + right: SegmentRef, + maxEndpointGap: number, +) => + Math.max(left.min, right.min) - Math.min(left.max, right.max) <= + maxEndpointGap + +const segmentsAreClusterable = ( + left: SegmentRef, + right: SegmentRef, + params: Required< + Pick< + AlignSameNetTraceSegmentsParams, + "maxAlignmentDistance" | "maxEndpointGap" + > + > & { + mergedLabelNetIdMap?: Record> + }, +) => + left.traceIndex !== right.traceIndex && + left.orientation === right.orientation && + areEquivalentNetIds(left.netId, right.netId, params.mergedLabelNetIdMap) && + Math.abs(left.fixedCoord - right.fixedCoord) <= params.maxAlignmentDistance && + rangesAreNear(left, right, params.maxEndpointGap) + +const getSegmentKey = (segment: SegmentRef) => + `${segment.traceIndex}:${segment.segmentIndex}` + +const getCandidateClusters = ( + segments: SegmentRef[], + params: Required< + Pick< + AlignSameNetTraceSegmentsParams, + "maxAlignmentDistance" | "maxEndpointGap" + > + > & { + mergedLabelNetIdMap?: Record> + }, +) => { + const visited = new Set() + const clusters: SegmentRef[][] = [] + + for (const segment of segments) { + const segmentKey = getSegmentKey(segment) + if (visited.has(segmentKey)) continue + + const cluster: SegmentRef[] = [] + const queue = [segment] + visited.add(segmentKey) + + while (queue.length > 0) { + const current = queue.shift()! + cluster.push(current) + + for (const candidate of segments) { + const candidateKey = getSegmentKey(candidate) + if (visited.has(candidateKey)) continue + if (!segmentsAreClusterable(current, candidate, params)) continue + + visited.add(candidateKey) + queue.push(candidate) + } + } + + if (cluster.length > 1) clusters.push(cluster) + } + + return clusters.sort((left, right) => { + const rightLength = right.reduce((sum, segment) => sum + segment.length, 0) + const leftLength = left.reduce((sum, segment) => sum + segment.length, 0) + return right.length - left.length || rightLength - leftLength + }) +} + +const getAnchorSegment = (cluster: SegmentRef[]) => + cluster.reduce((best, segment) => { + if (segment.length > best.length + EPS) return segment + if ( + Math.abs(segment.length - best.length) < EPS && + segment.traceIndex < best.traceIndex + ) { + return segment + } + return best + }, cluster[0]!) + +const rectFromSegment = ( + start: Point, + end: Point, + chipId: string, +): RectObstacle => ({ + chipId, + minX: Math.min(start.x, end.x) - TRACE_WIDTH / 2, + minY: Math.min(start.y, end.y) - TRACE_WIDTH / 2, + maxX: Math.max(start.x, end.x) + TRACE_WIDTH / 2, + maxY: Math.max(start.y, end.y) + TRACE_WIDTH / 2, +}) + +const getBlockingRectsForTrace = ( + traces: SolvedTracePath[], + traceIndex: number, + params: AlignSameNetTraceSegmentsOptions, +): RectObstacle[] => { + const targetTrace = traces[traceIndex]! + + const staticObstacles = + params.inputProblem?.chips === undefined + ? [] + : getObstacleRects(params.inputProblem).map((obstacle) => ({ + ...obstacle, + minX: obstacle.minX - STATIC_OBSTACLE_PADDING, + minY: obstacle.minY - STATIC_OBSTACLE_PADDING, + maxX: obstacle.maxX + STATIC_OBSTACLE_PADDING, + maxY: obstacle.maxY + STATIC_OBSTACLE_PADDING, + })) + + const traceObstacles = traces.flatMap((trace, otherTraceIndex) => { + if (otherTraceIndex === traceIndex) return [] + if ( + areEquivalentNetIds( + trace.globalConnNetId, + targetTrace.globalConnNetId, + params.mergedLabelNetIdMap, + ) + ) { + return [] + } + + return trace.tracePath.slice(0, -1).map((start, segmentIndex) => { + const end = trace.tracePath[segmentIndex + 1]! + return rectFromSegment( + start, + end, + `trace-${otherTraceIndex}-${segmentIndex}`, + ) + }) + }) + + const labelObstacles = (params.allLabelPlacements ?? []) + .filter( + (label) => + !areEquivalentNetIds( + label.globalConnNetId, + targetTrace.globalConnNetId, + params.mergedLabelNetIdMap, + ), + ) + .map((label) => ({ + chipId: `label-${label.globalConnNetId}`, + minX: label.center.x - label.width / 2 - (params.paddingBuffer ?? 0), + minY: label.center.y - label.height / 2 - (params.paddingBuffer ?? 0), + maxX: label.center.x + label.width / 2 + (params.paddingBuffer ?? 0), + maxY: label.center.y + label.height / 2 + (params.paddingBuffer ?? 0), + })) + + return [...staticObstacles, ...traceObstacles, ...labelObstacles] +} + +const countIntersections = (path: Point[], obstacles: RectObstacle[]) => { + let intersections = 0 + + for (let i = 0; i < path.length - 1; i++) { + const start = path[i]! + const end = path[i + 1]! + for (const obstacle of obstacles) { + if (segmentIntersectsRect(start, end, obstacle)) { + intersections++ + } + } + } + + return intersections +} + +const getMovedPath = ( + trace: SolvedTracePath, + segment: SegmentRef, + targetFixedCoord: number, +) => { + const nextPath = trace.tracePath.map((point) => ({ ...point })) + const start = nextPath[segment.segmentIndex]! + const end = nextPath[segment.segmentIndex + 1]! + + if (segment.orientation === "horizontal") { + start.y = targetFixedCoord + end.y = targetFixedCoord + } else { + start.x = targetFixedCoord + end.x = targetFixedCoord + } + + return normalizePath(nextPath) +} + +const canMoveSegment = ( + traces: SolvedTracePath[], + segment: SegmentRef, + targetFixedCoord: number, + params: AlignSameNetTraceSegmentsOptions, +) => { + const trace = traces[segment.traceIndex]! + const movedPath = getMovedPath(trace, segment, targetFixedCoord) + const blockingRects = getBlockingRectsForTrace( + traces, + segment.traceIndex, + params, + ) + + return ( + countIntersections(movedPath, blockingRects) <= + countIntersections(trace.tracePath, blockingRects) + ) +} + +const applySegmentMove = ( + traces: SolvedTracePath[], + segment: SegmentRef, + targetFixedCoord: number, +) => { + const trace = traces[segment.traceIndex]! + traces[segment.traceIndex] = { + ...trace, + tracePath: getMovedPath(trace, segment, targetFixedCoord), + } +} + +export const alignSameNetTraceSegments = ({ + traces, + maxAlignmentDistance = 0.12, + maxEndpointGap = 0.05, + maxIterations = 25, + ...params +}: AlignSameNetTraceSegmentsParams) => { + const outputTraces = traces.map(cloneTrace) + let changed = false + + for (let iteration = 0; iteration < maxIterations; iteration++) { + const segments = outputTraces.flatMap((_, traceIndex) => + collectInteriorSegments(outputTraces, traceIndex), + ) + const clusters = getCandidateClusters(segments, { + maxAlignmentDistance, + maxEndpointGap, + mergedLabelNetIdMap: params.mergedLabelNetIdMap, + }) + + let changedThisIteration = false + + for (const cluster of clusters) { + const anchor = getAnchorSegment(cluster) + for (const segment of cluster) { + if (getSegmentKey(segment) === getSegmentKey(anchor)) continue + if (Math.abs(segment.fixedCoord - anchor.fixedCoord) < EPS) continue + if (!canMoveSegment(outputTraces, segment, anchor.fixedCoord, params)) { + continue + } + + applySegmentMove(outputTraces, segment, anchor.fixedCoord) + changed = true + changedThisIteration = true + } + + if (changedThisIteration) break + } + + if (!changedThisIteration) break + } + + return { + changed, + traces: outputTraces, + } +} diff --git a/tests/examples/__snapshots__/example18.snap.svg b/tests/examples/__snapshots__/example18.snap.svg index 8c7f05fc8..bc8149cf2 100644 --- a/tests/examples/__snapshots__/example18.snap.svg +++ b/tests/examples/__snapshots__/example18.snap.svg @@ -65,24 +65,19 @@ y-" data-x="1.7580660749999977" data-y="-3.3025814000000002" cx="494.02875093834 y+" data-x="1.757519574999999" data-y="-2.2" cx="493.97982495355666" cy="501.29024555523955" r="3" fill="hsl(248, 100%, 50%, 0.8)" /> - + - + - + - + - + @@ -139,7 +134,7 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" - + @@ -167,28 +162,23 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" +globalConnNetId: connectivity_net0" data-x="-1.8574283249999997" data-y="0.9762093000000004" x="161.39522395803996" y="196.79342126794842" width="17.905209437554532" height="40.28672123449769" fill="#ef444466" stroke="#ef4444" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net0" data-x="1.5790330374999988" data-y="2.7275814000000005" x="469.0480260561722" y="40" width="17.90520943755456" height="40.28672123449769" fill="#ef444466" stroke="#ef4444" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net1" data-x="-2.31430995" data-y="-0.9762093000000004" x="120.4924180390637" y="371.58574098183345" width="17.905209437554532" height="40.28672123449769" fill="#00000066" stroke="#000000" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net2" data-x="1.982519574999999" data-y="0.85" x="493.97982495355666" y="219.2831969137558" width="40.28672123449769" height="17.905209437554532" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net3" data-x="1.982519574999999" data-y="-2" x="493.97982495355666" y="474.43243139890774" width="40.28672123449769" height="17.90520943755456" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011169933571428573" /> - + - + - + - + @@ -101,7 +97,7 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438 - + diff --git a/tests/solvers/TraceCleanupSolver/align-same-net-trace-segments.test.ts b/tests/solvers/TraceCleanupSolver/align-same-net-trace-segments.test.ts new file mode 100644 index 000000000..a94ceefeb --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/align-same-net-trace-segments.test.ts @@ -0,0 +1,183 @@ +import { expect, test } from "bun:test" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { alignSameNetTraceSegments } from "lib/solvers/TraceCleanupSolver/alignSameNetTraceSegments" +import type { InputProblem } from "lib/types/InputProblem" + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: Array<{ x: number; y: number }>, +): SolvedTracePath => + ({ + mspPairId, + globalConnNetId, + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [`${mspPairId}.1`, `${mspPairId}.2`], + pins: [], + }) as unknown as SolvedTracePath + +test("alignSameNetTraceSegments aligns a close same-net segment group to the longest segment", () => { + const longest = makeTrace("pair-a", "NET1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]) + const middle = makeTrace("pair-b", "NET1", [ + { x: 0, y: 0.06 }, + { x: 1.2, y: 0.06 }, + { x: 1.2, y: 1.06 }, + { x: 3.2, y: 1.06 }, + { x: 3.2, y: 0.06 }, + ]) + const shortest = makeTrace("pair-c", "NET1", [ + { x: 0, y: 0.11 }, + { x: 1.4, y: 0.11 }, + { x: 1.4, y: 1.11 }, + { x: 2.8, y: 1.11 }, + { x: 2.8, y: 0.11 }, + ]) + + const result = alignSameNetTraceSegments({ + traces: [middle, shortest, longest], + }) + + expect(result.changed).toBe(true) + expect(result.traces[0]!.tracePath[2]!.y).toBe(1) + expect(result.traces[0]!.tracePath[3]!.y).toBe(1) + expect(result.traces[1]!.tracePath[2]!.y).toBe(1) + expect(result.traces[1]!.tracePath[3]!.y).toBe(1) + expect(result.traces[2]!.tracePath).toEqual(longest.tracePath) +}) + +test("alignSameNetTraceSegments aligns close interior vertical segments", () => { + const left = makeTrace("pair-a", "NET1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 3 }, + { x: 0, y: 3 }, + ]) + const right = makeTrace("pair-b", "NET1", [ + { x: 0.08, y: 0 }, + { x: 1.08, y: 0 }, + { x: 1.08, y: 3 }, + { x: 0.08, y: 3 }, + ]) + + const result = alignSameNetTraceSegments({ traces: [left, right] }) + + expect(result.changed).toBe(true) + expect(result.traces[1]!.tracePath).toEqual([ + { x: 0.08, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 3 }, + { x: 0.08, y: 3 }, + ]) +}) + +test("alignSameNetTraceSegments does not align different nets", () => { + const first = makeTrace("pair-a", "NET1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 3, y: 1 }, + { x: 3, y: 0 }, + ]) + const second = makeTrace("pair-b", "NET2", [ + { x: 0, y: 0.08 }, + { x: 1.2, y: 0.08 }, + { x: 1.2, y: 1.08 }, + { x: 2.8, y: 1.08 }, + { x: 2.8, y: 0.08 }, + ]) + + const result = alignSameNetTraceSegments({ traces: [first, second] }) + + expect(result.changed).toBe(false) + expect(result.traces.map((trace) => trace.tracePath)).toEqual([ + first.tracePath, + second.tracePath, + ]) +}) + +test("alignSameNetTraceSegments leaves endpoint-only segments anchored", () => { + const first = makeTrace("pair-a", "NET1", [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 2, y: 1 }, + ]) + const second = makeTrace("pair-b", "NET1", [ + { x: 0, y: 0.08 }, + { x: 2, y: 0.08 }, + { x: 2, y: 1 }, + ]) + + const result = alignSameNetTraceSegments({ traces: [first, second] }) + + expect(result.changed).toBe(false) + expect(result.traces.map((trace) => trace.tracePath)).toEqual([ + first.tracePath, + second.tracePath, + ]) +}) + +test("alignSameNetTraceSegments does not align close segments within the same trace", () => { + const trace = makeTrace("pair-a", "NET1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 3, y: 1 }, + { x: 3, y: 1.08 }, + { x: 1, y: 1.08 }, + { x: 1, y: 2 }, + ]) + + const result = alignSameNetTraceSegments({ traces: [trace] }) + + expect(result.changed).toBe(false) + expect(result.traces[0]!.tracePath).toEqual(trace.tracePath) +}) + +test("alignSameNetTraceSegments rejects moves that add chip intersections", () => { + const keep = makeTrace("pair-a", "NET1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 3, y: 1 }, + { x: 3, y: 0 }, + ]) + const move = makeTrace("pair-b", "NET1", [ + { x: 0, y: 0.08 }, + { x: 1.2, y: 0.08 }, + { x: 1.2, y: 1.08 }, + { x: 2.8, y: 1.08 }, + { x: 2.8, y: 0.08 }, + ]) + const inputProblem: InputProblem = { + chips: [ + { + chipId: "obstacle", + center: { x: 2, y: 1 }, + width: 1, + height: 0.08, + pins: [], + }, + ], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + } + + const result = alignSameNetTraceSegments({ + traces: [keep, move], + inputProblem, + }) + + expect(result.changed).toBe(false) + expect(result.traces.map((trace) => trace.tracePath)).toEqual([ + keep.tracePath, + move.tracePath, + ]) +})