diff --git a/lib/index.ts b/lib/index.ts index 3985b32..6d005ab 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,4 @@ export * from "./solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" export * from "./types/InputProblem" export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2" +export * from "./solvers/TraceLineMergerSolver" diff --git a/lib/solvers/TraceLineMergerSolver/TraceLineMergerSolver.ts b/lib/solvers/TraceLineMergerSolver/TraceLineMergerSolver.ts new file mode 100644 index 0000000..b77deb8 --- /dev/null +++ b/lib/solvers/TraceLineMergerSolver/TraceLineMergerSolver.ts @@ -0,0 +1,487 @@ +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" +import type { GraphicsObject } from "graphics-debug" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { Point } from "@tscircuit/math-utils" + +export interface MergedTracePath extends SolvedTracePath { + originalTracePaths: SolvedTracePath[] + mergedSegments: MergedSegment[] +} + +export interface MergedSegment { + start: Point + end: Point + originalSegments: Array<{ + tracePath: SolvedTracePath + segmentIndex: number + }> +} + +export class TraceLineMergerSolver extends BaseSolver { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + mergedTracePaths: MergedTracePath[] = [] + maxMergeDistance: number = 0.1 // Maximum distance for merging lines + + constructor(params: { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + maxMergeDistance?: number + }) { + super() + this.inputProblem = params.inputProblem + this.inputTracePaths = params.inputTracePaths + this.maxMergeDistance = params.maxMergeDistance ?? 0.1 + } + + override getConstructorParams(): ConstructorParameters< + typeof TraceLineMergerSolver + >[0] { + return { + inputProblem: this.inputProblem, + inputTracePaths: this.inputTracePaths, + maxMergeDistance: this.maxMergeDistance, + } + } + + override _step() { + if (this.solved) return + + // Group trace paths by net + const netGroups = this.groupTracePathsByNet() + + // Process each net group + for (const [netId, tracePaths] of netGroups) { + const mergedPaths = this.mergeTracePathsInNet(tracePaths) + this.mergedTracePaths.push(...mergedPaths) + } + + this.solved = true + } + + private groupTracePathsByNet(): Map { + const netGroups = new Map() + + for (const tracePath of this.inputTracePaths) { + const netId = tracePath.userNetId || tracePath.dcConnNetId || tracePath.globalConnNetId || "unknown" + if (!netGroups.has(netId)) { + netGroups.set(netId, []) + } + netGroups.get(netId)!.push(tracePath) + } + + return netGroups + } + + private mergeTracePathsInNet(tracePaths: SolvedTracePath[]): MergedTracePath[] { + if (tracePaths.length <= 1) { + return tracePaths.map(tp => ({ + ...tp, + originalTracePaths: [tp], + mergedSegments: this.createSegmentsFromTracePath(tp) + })) + } + + // Extract all line segments from trace paths + const allSegments = this.extractSegmentsFromTracePaths(tracePaths) + + // Find mergeable segments + const mergedSegments = this.findAndMergeSegments(allSegments) + + // If no segments were merged, return original trace paths + if (mergedSegments.length === allSegments.length) { + return tracePaths.map(tp => ({ + ...tp, + originalTracePaths: [tp], + mergedSegments: this.createSegmentsFromTracePath(tp) + })) + } + + // Create a single merged trace path from all merged segments + return this.createSingleMergedTracePath(mergedSegments, tracePaths) + } + + private extractSegmentsFromTracePaths(tracePaths: SolvedTracePath[]): Array<{ + tracePath: SolvedTracePath + segmentIndex: number + start: Point + end: Point + }> { + const segments: Array<{ + tracePath: SolvedTracePath + segmentIndex: number + start: Point + end: Point + }> = [] + + for (const tracePath of tracePaths) { + const path = tracePath.tracePath + for (let i = 0; i < path.length - 1; i++) { + segments.push({ + tracePath, + segmentIndex: i, + start: path[i], + end: path[i + 1] + }) + } + } + + return segments + } + + private findAndMergeSegments(segments: Array<{ + tracePath: SolvedTracePath + segmentIndex: number + start: Point + end: Point + }>): MergedSegment[] { + const mergedSegments: MergedSegment[] = [] + const processed = new Set() + + for (let i = 0; i < segments.length; i++) { + if (processed.has(i)) continue + + const currentSegment = segments[i] + const mergeableSegments = [currentSegment] + processed.add(i) + + // Look for mergeable segments + for (let j = i + 1; j < segments.length; j++) { + if (processed.has(j)) continue + + const otherSegment = segments[j] + if (this.areSegmentsMergeable(currentSegment, otherSegment)) { + mergeableSegments.push(otherSegment) + processed.add(j) + } + } + + // Create merged segment + const mergedSegment = this.createMergedSegment(mergeableSegments) + mergedSegments.push(mergedSegment) + } + + return mergedSegments + } + + private areSegmentsMergeable( + seg1: { start: Point; end: Point }, + seg2: { start: Point; end: Point } + ): boolean { + // Check if segments are horizontally aligned (same Y, close X) + if (this.areHorizontallyAligned(seg1, seg2)) { + return this.areHorizontallyMergeable(seg1, seg2) + } + + // Check if segments are vertically aligned (same X, close Y) + if (this.areVerticallyAligned(seg1, seg2)) { + return this.areVerticallyMergeable(seg1, seg2) + } + + return false + } + + private areHorizontallyAligned( + seg1: { start: Point; end: Point }, + seg2: { start: Point; end: Point } + ): boolean { + const y1 = seg1.start.y + const y2 = seg2.start.y + return Math.abs(y1 - y2) < this.maxMergeDistance + } + + private areVerticallyAligned( + seg1: { start: Point; end: Point }, + seg2: { start: Point; end: Point } + ): boolean { + const x1 = seg1.start.x + const x2 = seg2.start.x + return Math.abs(x1 - x2) < this.maxMergeDistance + } + + private areHorizontallyMergeable( + seg1: { start: Point; end: Point }, + seg2: { start: Point; end: Point } + ): boolean { + // Check if segments overlap or are adjacent in X direction + const seg1MinX = Math.min(seg1.start.x, seg1.end.x) + const seg1MaxX = Math.max(seg1.start.x, seg1.end.x) + const seg2MinX = Math.min(seg2.start.x, seg2.end.x) + const seg2MaxX = Math.max(seg2.start.x, seg2.end.x) + + // Check for overlap + const overlap = Math.min(seg1MaxX, seg2MaxX) - Math.max(seg1MinX, seg2MinX) + if (overlap >= 0) { + return true // Segments overlap + } + + // Check the gap between segments + const gap = Math.max(seg1MinX, seg2MinX) - Math.min(seg1MaxX, seg2MaxX) + return gap <= this.maxMergeDistance + } + + private areVerticallyMergeable( + seg1: { start: Point; end: Point }, + seg2: { start: Point; end: Point } + ): boolean { + // Check if segments overlap or are adjacent in Y direction + const seg1MinY = Math.min(seg1.start.y, seg1.end.y) + const seg1MaxY = Math.max(seg1.start.y, seg1.end.y) + const seg2MinY = Math.min(seg2.start.y, seg2.end.y) + const seg2MaxY = Math.max(seg2.start.y, seg2.end.y) + + // Check for overlap + const overlap = Math.min(seg1MaxY, seg2MaxY) - Math.max(seg1MinY, seg2MinY) + if (overlap >= 0) { + return true // Segments overlap + } + + // Check the gap between segments + const gap = Math.max(seg1MinY, seg2MinY) - Math.min(seg1MaxY, seg2MaxY) + return gap <= this.maxMergeDistance + } + + private createMergedSegment(segments: Array<{ + tracePath: SolvedTracePath + segmentIndex: number + start: Point + end: Point + }>): MergedSegment { + if (segments.length === 1) { + const seg = segments[0] + return { + start: seg.start, + end: seg.end, + originalSegments: [{ + tracePath: seg.tracePath, + segmentIndex: seg.segmentIndex + }] + } + } + + // Determine if segments are horizontal or vertical + const isHorizontal = this.areHorizontallyAligned(segments[0], segments[1]) + + if (isHorizontal) { + // Merge horizontally aligned segments + const allX = segments.flatMap(s => [s.start.x, s.end.x]) + const allY = segments.flatMap(s => [s.start.y, s.end.y]) + + const minX = Math.min(...allX) + const maxX = Math.max(...allX) + // Use the Y coordinate from the first segment instead of averaging + const y = segments[0].start.y + + return { + start: { x: minX, y }, + end: { x: maxX, y }, + originalSegments: segments.map(s => ({ + tracePath: s.tracePath, + segmentIndex: s.segmentIndex + })) + } + } else { + // Merge vertically aligned segments + const allX = segments.flatMap(s => [s.start.x, s.end.x]) + const allY = segments.flatMap(s => [s.start.y, s.end.y]) + + const minY = Math.min(...allY) + const maxY = Math.max(...allY) + // Use the X coordinate from the first segment instead of averaging + const x = segments[0].start.x + + return { + start: { x, y: minY }, + end: { x, y: maxY }, + originalSegments: segments.map(s => ({ + tracePath: s.tracePath, + segmentIndex: s.segmentIndex + })) + } + } + } + + private createSingleMergedTracePath( + mergedSegments: MergedSegment[], + originalTracePaths: SolvedTracePath[] + ): MergedTracePath[] { + if (mergedSegments.length === 0) { + return [] + } + + // Create a merged path by intelligently combining the original paths + const tracePath = this.reconstructMergedPath(mergedSegments, originalTracePaths) + + // Use the first original trace path as the base + const baseTracePath = originalTracePaths[0] + + return [{ + ...baseTracePath, + tracePath, + originalTracePaths, + mergedSegments + }] + } + + private reconstructMergedPath( + mergedSegments: MergedSegment[], + originalTracePaths: SolvedTracePath[] + ): Point[] { + // For complex multi-segment paths, we need to be more careful + // Let's start with a simple approach: combine all paths and then apply merges + + const allPoints: Point[] = [] + + // Collect all points from all trace paths + for (const tracePath of originalTracePaths) { + allPoints.push(...tracePath.tracePath) + } + + // Apply merged segments to replace overlapping/close segments + return this.applyMergesToPoints(allPoints, mergedSegments) + } + + private applyMergesToPoints(points: Point[], mergedSegments: MergedSegment[]): Point[] { + // For each merged segment, find the corresponding points in the path and replace them + let result = [...points] + + for (const mergedSegment of mergedSegments) { + // Find the start and end points of the merged segment + const start = mergedSegment.start + const end = mergedSegment.end + + // Find the indices of these points in the result + const startIndex = result.findIndex(p => this.distance(p, start) < 0.001) + const endIndex = result.findIndex(p => this.distance(p, end) < 0.001) + + if (startIndex >= 0 && endIndex >= 0 && startIndex < endIndex) { + // Replace the segment with the merged segment + result = [ + ...result.slice(0, startIndex), + start, + end, + ...result.slice(endIndex + 1) + ] + } + } + + return this.removeDuplicateConsecutivePoints(result) + } + + private removeDuplicateConsecutivePoints(points: Point[]): Point[] { + if (points.length <= 1) return points + + const result: Point[] = [points[0]] + + for (let i = 1; i < points.length; i++) { + const current = points[i] + const previous = result[result.length - 1] + + if (this.distance(current, previous) > 0.001) { + result.push(current) + } + } + + return result + } + + private sortSegmentsForPath(segments: MergedSegment[]): MergedSegment[] { + if (segments.length <= 1) return segments + + // Simple sorting: connect segments end-to-start + const sorted: MergedSegment[] = [] + const remaining = [...segments] + + // Start with the first segment + sorted.push(remaining.shift()!) + + while (remaining.length > 0) { + const lastEnd = sorted[sorted.length - 1].end + let bestMatchIndex = -1 + let bestDistance = Infinity + + for (let i = 0; i < remaining.length; i++) { + const distance = this.distance(lastEnd, remaining[i].start) + if (distance < bestDistance) { + bestDistance = distance + bestMatchIndex = i + } + } + + if (bestMatchIndex >= 0) { + sorted.push(remaining.splice(bestMatchIndex, 1)[0]) + } else { + // If no good match, just add the first remaining segment + sorted.push(remaining.shift()!) + } + } + + return sorted + } + + private createPathFromSegments(segments: MergedSegment[]): Point[] { + if (segments.length === 0) return [] + + const path: Point[] = [segments[0].start] + + for (const segment of segments) { + path.push(segment.end) + } + + return path + } + + private createSegmentsFromTracePath(tracePath: SolvedTracePath): MergedSegment[] { + const segments: MergedSegment[] = [] + const path = tracePath.tracePath + + for (let i = 0; i < path.length - 1; i++) { + segments.push({ + start: path[i], + end: path[i + 1], + originalSegments: [{ + tracePath, + segmentIndex: i + }] + }) + } + + return segments + } + + private distance(p1: Point, p2: Point): number { + const dx = p1.x - p2.x + const dy = p1.y - p2.y + return Math.sqrt(dx * dx + dy * dy) + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem, { + chipAlpha: 0.1, + connectionAlpha: 0.1, + }) + + // Draw original trace paths in light gray + for (const tracePath of this.inputTracePaths) { + graphics.lines!.push({ + points: tracePath.tracePath, + strokeColor: "lightgray", + strokeDash: "2 2", + strokeWidth: 1, + }) + } + + // Draw merged trace paths in green + for (const mergedPath of this.mergedTracePaths) { + graphics.lines!.push({ + points: mergedPath.tracePath, + strokeColor: "green", + strokeWidth: 2, + }) + } + + return graphics + } +} diff --git a/lib/solvers/TraceLineMergerSolver/index.ts b/lib/solvers/TraceLineMergerSolver/index.ts new file mode 100644 index 0000000..68df640 --- /dev/null +++ b/lib/solvers/TraceLineMergerSolver/index.ts @@ -0,0 +1,2 @@ +export { TraceLineMergerSolver } from "./TraceLineMergerSolver" +export type { MergedTracePath, MergedSegment } from "./TraceLineMergerSolver" diff --git a/site/TraceLineMergerSolver/TraceLineMergerSolver01.page.tsx b/site/TraceLineMergerSolver/TraceLineMergerSolver01.page.tsx new file mode 100644 index 0000000..4b03694 --- /dev/null +++ b/site/TraceLineMergerSolver/TraceLineMergerSolver01.page.tsx @@ -0,0 +1,140 @@ +import { GenericSolverDebugger } from "site/components/GenericSolverDebugger" +import { TraceLineMergerSolver } from "lib/solvers/TraceLineMergerSolver/TraceLineMergerSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { useMemo } from "react" + +export const inputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: 0, y: 0 }, + width: 1.6, + height: 0.6, + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2 }, + { pinId: "U1.2", x: -0.8, y: 0 }, + { pinId: "U1.3", x: -0.8, y: -0.2 }, + { pinId: "U1.4", x: 0.8, y: -0.2 }, + { pinId: "U1.5", x: 0.8, y: 0 }, + { pinId: "U1.6", x: 0.8, y: 0.2 }, + ], + }, + { + chipId: "U2", + center: { x: 3, y: 0 }, + width: 1.6, + height: 0.6, + pins: [ + { pinId: "U2.1", x: 2.2, y: 0.2 }, + { pinId: "U2.2", x: 2.2, y: 0 }, + { pinId: "U2.3", x: 2.2, y: -0.2 }, + { pinId: "U2.4", x: 3.8, y: -0.2 }, + { pinId: "U2.5", x: 3.8, y: 0 }, + { pinId: "U2.6", x: 3.8, y: 0.2 }, + ], + }, + ], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +// Create test trace paths with close lines that should be merged +const createTestTracePaths = (): SolvedTracePath[] => [ + // Horizontal segments on the same Y level - should be merged + { + mspPairId: "pair1", + dcConnNetId: "VCC", + globalConnNetId: "VCC", + userNetId: "VCC", + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2, chipId: "U1" }, + { pinId: "U2.1", x: 2.2, y: 0.2, chipId: "U2" }, + ], + tracePath: [ + { x: -0.8, y: 0.2 }, + { x: 0.5, y: 0.2 }, + ], + mspConnectionPairIds: ["pair1"], + pinIds: ["U1.1", "U2.1"], + }, + { + mspPairId: "pair2", + dcConnNetId: "VCC", + globalConnNetId: "VCC", + userNetId: "VCC", + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2, chipId: "U1" }, + { pinId: "U2.1", x: 2.2, y: 0.2, chipId: "U2" }, + ], + tracePath: [ + { x: 0.52, y: 0.2 }, // Small gap from previous segment + { x: 2.2, y: 0.2 }, + ], + mspConnectionPairIds: ["pair2"], + pinIds: ["U1.1", "U2.1"], + }, + // Vertical segments on the same X level - should be merged + { + mspPairId: "pair3", + dcConnNetId: "GND", + globalConnNetId: "GND", + userNetId: "GND", + pins: [ + { pinId: "U1.3", x: -0.8, y: -0.2, chipId: "U1" }, + { pinId: "U2.3", x: 2.2, y: -0.2, chipId: "U2" }, + ], + tracePath: [ + { x: 1, y: -0.2 }, + { x: 1, y: 0.5 }, + ], + mspConnectionPairIds: ["pair3"], + pinIds: ["U1.3", "U2.3"], + }, + { + mspPairId: "pair4", + dcConnNetId: "GND", + globalConnNetId: "GND", + userNetId: "GND", + pins: [ + { pinId: "U1.3", x: -0.8, y: -0.2, chipId: "U1" }, + { pinId: "U2.3", x: 2.2, y: -0.2, chipId: "U2" }, + ], + tracePath: [ + { x: 1.02, y: 0.5 }, // Small gap from previous segment + { x: 1, y: 1.5 }, + ], + mspConnectionPairIds: ["pair4"], + pinIds: ["U1.3", "U2.3"], + }, + // Non-mergeable segment (different net) + { + mspPairId: "pair5", + dcConnNetId: "SIGNAL", + globalConnNetId: "SIGNAL", + userNetId: "SIGNAL", + pins: [ + { pinId: "U1.2", x: -0.8, y: 0, chipId: "U1" }, + { pinId: "U2.2", x: 2.2, y: 0, chipId: "U2" }, + ], + tracePath: [ + { x: -0.8, y: 0 }, + { x: 2.2, y: 0 }, + ], + mspConnectionPairIds: ["pair5"], + pinIds: ["U1.2", "U2.2"], + }, +] + +export default () => { + const solver = useMemo(() => { + return new TraceLineMergerSolver({ + inputProblem, + inputTracePaths: createTestTracePaths(), + maxMergeDistance: 0.1, + }) + }, []) + + return +} diff --git a/site/TraceLineMergerSolver/TraceLineMergerSolver02.page.tsx b/site/TraceLineMergerSolver/TraceLineMergerSolver02.page.tsx new file mode 100644 index 0000000..a8f1dab --- /dev/null +++ b/site/TraceLineMergerSolver/TraceLineMergerSolver02.page.tsx @@ -0,0 +1,170 @@ +import { GenericSolverDebugger } from "site/components/GenericSolverDebugger" +import { TraceLineMergerSolver } from "lib/solvers/TraceLineMergerSolver/TraceLineMergerSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { useMemo } from "react" + +export const inputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: 0, y: 0 }, + width: 1.6, + height: 0.6, + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2 }, + { pinId: "U1.2", x: -0.8, y: 0 }, + { pinId: "U1.3", x: -0.8, y: -0.2 }, + { pinId: "U1.4", x: 0.8, y: -0.2 }, + { pinId: "U1.5", x: 0.8, y: 0 }, + { pinId: "U1.6", x: 0.8, y: 0.2 }, + ], + }, + { + chipId: "U2", + center: { x: 4, y: 0 }, + width: 1.6, + height: 0.6, + pins: [ + { pinId: "U2.1", x: 3.2, y: 0.2 }, + { pinId: "U2.2", x: 3.2, y: 0 }, + { pinId: "U2.3", x: 3.2, y: -0.2 }, + { pinId: "U2.4", x: 4.8, y: -0.2 }, + { pinId: "U2.5", x: 4.8, y: 0 }, + { pinId: "U2.6", x: 4.8, y: 0.2 }, + ], + }, + { + chipId: "U3", + center: { x: 2, y: 2 }, + width: 1.6, + height: 0.6, + pins: [ + { pinId: "U3.1", x: 1.2, y: 2.2 }, + { pinId: "U3.2", x: 1.2, y: 2 }, + { pinId: "U3.3", x: 1.2, y: 1.8 }, + { pinId: "U3.4", x: 2.8, y: 1.8 }, + { pinId: "U3.5", x: 2.8, y: 2 }, + { pinId: "U3.6", x: 2.8, y: 2.2 }, + ], + }, + ], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +// Create test trace paths with overlapping segments +const createTestTracePaths = (): SolvedTracePath[] => [ + // Overlapping horizontal segments - should be merged + { + mspPairId: "pair1", + dcConnNetId: "VCC", + globalConnNetId: "VCC", + userNetId: "VCC", + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2, chipId: "U1" }, + { pinId: "U2.1", x: 3.2, y: 0.2, chipId: "U2" }, + ], + tracePath: [ + { x: -0.8, y: 0.2 }, + { x: 1.5, y: 0.2 }, + ], + mspConnectionPairIds: ["pair1"], + pinIds: ["U1.1", "U2.1"], + }, + { + mspPairId: "pair2", + dcConnNetId: "VCC", + globalConnNetId: "VCC", + userNetId: "VCC", + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2, chipId: "U1" }, + { pinId: "U2.1", x: 3.2, y: 0.2, chipId: "U2" }, + ], + tracePath: [ + { x: 1.2, y: 0.2 }, // Overlaps with previous segment + { x: 3.2, y: 0.2 }, + ], + mspConnectionPairIds: ["pair2"], + pinIds: ["U1.1", "U2.1"], + }, + // Multiple vertical segments that should be merged + { + mspPairId: "pair3", + dcConnNetId: "GND", + globalConnNetId: "GND", + userNetId: "GND", + pins: [ + { pinId: "U1.3", x: -0.8, y: -0.2, chipId: "U1" }, + { pinId: "U3.3", x: 1.2, y: 1.8, chipId: "U3" }, + ], + tracePath: [ + { x: 0.5, y: -0.2 }, + { x: 0.5, y: 0.8 }, + ], + mspConnectionPairIds: ["pair3"], + pinIds: ["U1.3", "U3.3"], + }, + { + mspPairId: "pair4", + dcConnNetId: "GND", + globalConnNetId: "GND", + userNetId: "GND", + pins: [ + { pinId: "U1.3", x: -0.8, y: -0.2, chipId: "U1" }, + { pinId: "U3.3", x: 1.2, y: 1.8, chipId: "U3" }, + ], + tracePath: [ + { x: 0.52, y: 0.8 }, // Small gap + { x: 0.5, y: 1.8 }, + ], + mspConnectionPairIds: ["pair4"], + pinIds: ["U1.3", "U3.3"], + }, + // Segments too far apart - should not be merged + { + mspPairId: "pair5", + dcConnNetId: "SIGNAL", + globalConnNetId: "SIGNAL", + userNetId: "SIGNAL", + pins: [ + { pinId: "U1.2", x: -0.8, y: 0, chipId: "U1" }, + { pinId: "U2.2", x: 3.2, y: 0, chipId: "U2" }, + ], + tracePath: [ + { x: -0.8, y: 0 }, + { x: 0.5, y: 0 }, + ], + mspConnectionPairIds: ["pair5"], + pinIds: ["U1.2", "U2.2"], + }, + { + mspPairId: "pair6", + dcConnNetId: "SIGNAL", + globalConnNetId: "SIGNAL", + userNetId: "SIGNAL", + pins: [ + { pinId: "U1.2", x: -0.8, y: 0, chipId: "U1" }, + { pinId: "U2.2", x: 3.2, y: 0, chipId: "U2" }, + ], + tracePath: [ + { x: 2.5, y: 0 }, // Too far from previous segment + { x: 3.2, y: 0 }, + ], + mspConnectionPairIds: ["pair6"], + pinIds: ["U1.2", "U2.2"], + }, +] + +export default () => { + const solver = useMemo(() => { + return new TraceLineMergerSolver({ + inputProblem, + inputTracePaths: createTestTracePaths(), + maxMergeDistance: 0.1, + }) + }, []) + + return +} diff --git a/tests/solvers/TraceLineMergerSolver/TraceLineMergerSolver.test.ts b/tests/solvers/TraceLineMergerSolver/TraceLineMergerSolver.test.ts new file mode 100644 index 0000000..7590759 --- /dev/null +++ b/tests/solvers/TraceLineMergerSolver/TraceLineMergerSolver.test.ts @@ -0,0 +1,275 @@ +import { TraceLineMergerSolver } from "lib/solvers/TraceLineMergerSolver/TraceLineMergerSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +describe("TraceLineMergerSolver", () => { + const createTestInputProblem = (): InputProblem => ({ + chips: [ + { + chipId: "U1", + center: { x: 0, y: 0 }, + width: 1.6, + height: 0.6, + pins: [ + { pinId: "U1.1", x: -0.8, y: 0.2 }, + { pinId: "U1.2", x: -0.8, y: 0 }, + { pinId: "U1.3", x: -0.8, y: -0.2 }, + { pinId: "U1.4", x: 0.8, y: -0.2 }, + { pinId: "U1.5", x: 0.8, y: 0 }, + { pinId: "U1.6", x: 0.8, y: 0.2 }, + ], + }, + ], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + }) + + const createTestTracePath = ( + tracePath: Array<{ x: number; y: number }>, + netId: string = "test_net" + ): SolvedTracePath => ({ + mspPairId: `pair_${Math.random()}`, + dcConnNetId: netId, + globalConnNetId: netId, + userNetId: netId, + pins: [ + { pinId: "pin1", x: tracePath[0].x, y: tracePath[0].y, chipId: "U1" }, + { pinId: "pin2", x: tracePath[tracePath.length - 1].x, y: tracePath[tracePath.length - 1].y, chipId: "U1" }, + ], + tracePath, + mspConnectionPairIds: [`pair_${Math.random()}`], + pinIds: ["pin1", "pin2"], + }) + + it("should merge horizontally aligned segments", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + // Two horizontal segments on the same Y level + createTestTracePath([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], "net1"), + createTestTracePath([ + { x: 2.05, y: 0 }, // Small gap + { x: 4, y: 0 }, + ], "net1"), + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + maxMergeDistance: 0.1, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(1) + + const mergedPath = solver.mergedTracePaths[0] + expect(mergedPath.tracePath).toEqual([ + { x: 0, y: 0 }, + { x: 4, y: 0 }, + ]) + expect(mergedPath.originalTracePaths).toHaveLength(2) + }) + + it("should merge vertically aligned segments", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + // Two vertical segments on the same X level + createTestTracePath([ + { x: 0, y: 0 }, + { x: 0, y: 2 }, + ], "net1"), + createTestTracePath([ + { x: 0.05, y: 2 }, // Small gap + { x: 0, y: 4 }, + ], "net1"), + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + maxMergeDistance: 0.1, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(1) + + const mergedPath = solver.mergedTracePaths[0] + expect(mergedPath.tracePath).toEqual([ + { x: 0, y: 0 }, + { x: 0, y: 4 }, + ]) + expect(mergedPath.originalTracePaths).toHaveLength(2) + }) + + it("should not merge segments from different nets", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + createTestTracePath([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], "net1"), + createTestTracePath([ + { x: 2.05, y: 0 }, + { x: 4, y: 0 }, + ], "net2"), // Different net + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + maxMergeDistance: 0.1, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(2) // Should remain separate + }) + + it("should not merge segments that are too far apart", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + createTestTracePath([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], "net1"), + createTestTracePath([ + { x: 3, y: 0 }, // Too far apart + { x: 5, y: 0 }, + ], "net1"), + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + maxMergeDistance: 0.1, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(2) // Should remain separate + }) + + it("should handle overlapping segments", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + createTestTracePath([ + { x: 0, y: 0 }, + { x: 3, y: 0 }, + ], "net1"), + createTestTracePath([ + { x: 2, y: 0 }, // Overlaps with first segment + { x: 4, y: 0 }, + ], "net1"), + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + maxMergeDistance: 0.1, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(1) + + const mergedPath = solver.mergedTracePaths[0] + expect(mergedPath.tracePath).toEqual([ + { x: 0, y: 0 }, + { x: 4, y: 0 }, + ]) + }) + + it("should handle complex multi-segment paths", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + createTestTracePath([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + { x: 2, y: 2 }, + ], "net1"), + createTestTracePath([ + { x: 2.05, y: 2 }, // Small gap + { x: 4, y: 2 }, + ], "net1"), + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + maxMergeDistance: 0.1, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(1) + + const mergedPath = solver.mergedTracePaths[0] + // Should merge the horizontal segments at y=2 + // The exact path structure may vary, but it should contain the merged segment + expect(mergedPath.tracePath.length).toBeGreaterThanOrEqual(3) + + // Check that the merged segment (2,2) to (4,2) is present + const distance = (p1: { x: number; y: number }, p2: { x: number; y: number }) => + Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) + + const hasMergedSegment = mergedPath.tracePath.some((point, index) => { + if (index < mergedPath.tracePath.length - 1) { + const nextPoint = mergedPath.tracePath[index + 1] + return distance(point, { x: 2, y: 2 }) < 0.1 && + distance(nextPoint, { x: 4, y: 2 }) < 0.1 + } + return false + }) + expect(hasMergedSegment).toBe(true) + }) + + it("should handle empty input", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(0) + }) + + it("should handle single trace path", () => { + const inputProblem = createTestInputProblem() + const inputTracePaths: SolvedTracePath[] = [ + createTestTracePath([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], "net1"), + ] + + const solver = new TraceLineMergerSolver({ + inputProblem, + inputTracePaths, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.mergedTracePaths).toHaveLength(1) + expect(solver.mergedTracePaths[0].tracePath).toEqual([ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ]) + }) +})