diff --git a/lib/solvers/SameNetTraceLineMergeSolver/SameNetTraceLineMergeSolver.ts b/lib/solvers/SameNetTraceLineMergeSolver/SameNetTraceLineMergeSolver.ts new file mode 100644 index 00000000..1324d141 --- /dev/null +++ b/lib/solvers/SameNetTraceLineMergeSolver/SameNetTraceLineMergeSolver.ts @@ -0,0 +1,508 @@ +import type { Point } from "@tscircuit/math-utils" +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { MspConnectionPairId } from "../MspConnectionPairSolver/MspConnectionPairSolver" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" + +type HorzSeg = { + mspPairId: MspConnectionPairId + segIdx: number + start: number + end: number + canonY: number + bucket: number +} + +type VertSeg = { + mspPairId: MspConnectionPairId + segIdx: number + start: number + end: number + canonX: number + bucket: number +} + +function bucketCoord(canon: number, tolerance: number): number { + return Math.round(canon / tolerance) * tolerance +} + +/** Merge bucket keys that are within `tolerance` so near-Y segments (e.g. 1.0 vs 1.005) share one pass. */ +function clusterSortedBuckets( + sortedUnique: number[], + tolerance: number, +): number[][] { + if (sortedUnique.length === 0) return [] + const clusters: number[][] = [] + let cur = [sortedUnique[0]!] + for (let i = 1; i < sortedUnique.length; i++) { + const b = sortedUnique[i]! + if (b - cur[cur.length - 1]! <= tolerance) { + cur.push(b) + } else { + clusters.push(cur) + cur = [b] + } + } + clusters.push(cur) + return clusters +} + +function deepCloneTraceMap( + map: Record, +): Record { + const out: Record = {} + for (const id of Object.keys(map)) { + const t = map[id]! + out[id] = { + ...t, + pins: structuredClone(t.pins), + tracePath: t.tracePath.map((p) => ({ x: p.x, y: p.y })), + mspConnectionPairIds: [...t.mspConnectionPairIds], + pinIds: [...t.pinIds], + } + } + return out +} + +function classifyHorizontal( + p0: Point, + p1: Point, + tolerance: number, +): { canonY: number } | null { + const dy = Math.abs(p0.y - p1.y) + const dx = Math.abs(p0.x - p1.x) + if (dy < tolerance && dx < tolerance) return null + if (dy < tolerance) { + return { canonY: (p0.y + p1.y) / 2 } + } + return null +} + +function classifyVertical( + p0: Point, + p1: Point, + tolerance: number, +): { canonX: number } | null { + const dy = Math.abs(p0.y - p1.y) + const dx = Math.abs(p0.x - p1.x) + if (dy < tolerance && dx < tolerance) return null + if (dx < tolerance) { + return { canonX: (p0.x + p1.x) / 2 } + } + return null +} + +function orderHorizontalEndpoints( + prev: Point | null, + next: Point | null, + xmin: number, + xmax: number, + y: number, +): [Point, Point] { + const a: Point = { x: xmin, y } + const b: Point = { x: xmax, y } + if (!prev && !next) return [a, b] + const dist = (p: Point, q: Point) => + (p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y) + if (prev && !next) { + return dist(prev, a) <= dist(prev, b) ? [a, b] : [b, a] + } + if (!prev && next) { + return dist(a, next) <= dist(b, next) ? [a, b] : [b, a] + } + const s1 = dist(prev!, a) + dist(b, next!) + const s2 = dist(prev!, b) + dist(a, next!) + return s1 <= s2 ? [a, b] : [b, a] +} + +function orderVerticalEndpoints( + prev: Point | null, + next: Point | null, + ymin: number, + ymax: number, + x: number, +): [Point, Point] { + const a: Point = { x, y: ymin } + const b: Point = { x, y: ymax } + if (!prev && !next) return [a, b] + const dist = (p: Point, q: Point) => + (p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y) + if (prev && !next) { + return dist(prev, a) <= dist(prev, b) ? [a, b] : [b, a] + } + if (!prev && next) { + return dist(a, next) <= dist(b, next) ? [a, b] : [b, a] + } + const s1 = dist(prev!, a) + dist(b, next!) + const s2 = dist(prev!, b) + dist(a, next!) + return s1 <= s2 ? [a, b] : [b, a] +} + +function spliceSegmentChain( + pts: Point[], + segFirst: number, + segLast: number, + pStart: Point, + pEnd: Point, +): Point[] { + const before = pts.slice(0, segFirst) + const after = pts.slice(segLast + 2) + return [...before, pStart, pEnd, ...after] +} + +type HMergeOp = { + mspPairId: MspConnectionPairId + segFirst: number + segLast: number + x0: number + x1: number + y: number +} + +type VMergeOp = { + mspPairId: MspConnectionPairId + segFirst: number + segLast: number + y0: number + y1: number + x: number +} + +function collectHorizontalMergeOps( + traces: Record, + tolerance: number, +): HMergeOp[] { + const byNet = new Map() + for (const mspPairId of Object.keys(traces)) { + const path = traces[mspPairId]!.tracePath + const net = traces[mspPairId]!.globalConnNetId + for (let i = 0; i < path.length - 1; i++) { + const p0 = path[i]! + const p1 = path[i + 1]! + const h = classifyHorizontal(p0, p1, tolerance) + if (!h) continue + const bucket = bucketCoord(h.canonY, tolerance) + const start = Math.min(p0.x, p1.x) + const end = Math.max(p0.x, p1.x) + const seg: HorzSeg = { + mspPairId, + segIdx: i, + start, + end, + canonY: h.canonY, + bucket, + } + const list = byNet.get(net) ?? [] + list.push(seg) + byNet.set(net, list) + } + } + + const ops: HMergeOp[] = [] + for (const segs of byNet.values()) { + const uniqueBuckets = [...new Set(segs.map((s) => s.bucket))].sort( + (a, b) => a - b, + ) + const bucketClusters = clusterSortedBuckets(uniqueBuckets, tolerance) + const bucketToCluster = new Map() + for (let ci = 0; ci < bucketClusters.length; ci++) { + for (const bk of bucketClusters[ci]!) { + bucketToCluster.set(bk, ci) + } + } + for (let ci = 0; ci < bucketClusters.length; ci++) { + const clusterSegs = segs.filter( + (s) => bucketToCluster.get(s.bucket) === ci, + ) + clusterSegs.sort((a, b) => a.start - b.start) + let i = 0 + while (i < clusterSegs.length) { + let curEnd = clusterSegs[i]!.end + const group: HorzSeg[] = [clusterSegs[i]!] + let j = i + 1 + while ( + j < clusterSegs.length && + curEnd >= clusterSegs[j]!.start - tolerance + ) { + curEnd = Math.max(curEnd, clusterSegs[j]!.end) + group.push(clusterSegs[j]!) + j++ + } + if (group.length > 1) { + const x0 = Math.min(...group.map((g) => g.start)) + const x1 = Math.max(...group.map((g) => g.end)) + let sumY = 0 + let c = 0 + for (const g of group) { + const pts = traces[g.mspPairId]!.tracePath + const a = pts[g.segIdx]! + const b = pts[g.segIdx + 1]! + sumY += a.y + b.y + c += 2 + } + const y = sumY / c + const byPath = new Map() + for (const g of group) { + const arr = byPath.get(g.mspPairId) ?? [] + arr.push(g.segIdx) + byPath.set(g.mspPairId, arr) + } + for (const [msp, idxs] of byPath) { + idxs.sort((a, b) => a - b) + const chains: number[][] = [] + let chain = [idxs[0]!] + for (let k = 1; k < idxs.length; k++) { + if (idxs[k] === idxs[k - 1]! + 1) { + chain.push(idxs[k]!) + } else { + chains.push(chain) + chain = [idxs[k]!] + } + } + chains.push(chain) + for (const ch of chains) { + ops.push({ + mspPairId: msp, + segFirst: ch[0]!, + segLast: ch[ch.length - 1]!, + x0, + x1, + y, + }) + } + } + } + i = j + } + } + } + return ops +} + +function collectVerticalMergeOps( + traces: Record, + tolerance: number, +): VMergeOp[] { + const byNet = new Map() + for (const mspPairId of Object.keys(traces)) { + const path = traces[mspPairId]!.tracePath + const net = traces[mspPairId]!.globalConnNetId + for (let i = 0; i < path.length - 1; i++) { + const p0 = path[i]! + const p1 = path[i + 1]! + const v = classifyVertical(p0, p1, tolerance) + if (!v) continue + const h = classifyHorizontal(p0, p1, tolerance) + if (h) continue + const bucket = bucketCoord(v.canonX, tolerance) + const start = Math.min(p0.y, p1.y) + const end = Math.max(p0.y, p1.y) + const seg: VertSeg = { + mspPairId, + segIdx: i, + start, + end, + canonX: v.canonX, + bucket, + } + const list = byNet.get(net) ?? [] + list.push(seg) + byNet.set(net, list) + } + } + + const ops: VMergeOp[] = [] + for (const segs of byNet.values()) { + const uniqueBuckets = [...new Set(segs.map((s) => s.bucket))].sort( + (a, b) => a - b, + ) + const bucketClusters = clusterSortedBuckets(uniqueBuckets, tolerance) + const bucketToCluster = new Map() + for (let ci = 0; ci < bucketClusters.length; ci++) { + for (const bk of bucketClusters[ci]!) { + bucketToCluster.set(bk, ci) + } + } + for (let ci = 0; ci < bucketClusters.length; ci++) { + const clusterSegs = segs.filter( + (s) => bucketToCluster.get(s.bucket) === ci, + ) + clusterSegs.sort((a, b) => a.start - b.start) + let i = 0 + while (i < clusterSegs.length) { + let curEnd = clusterSegs[i]!.end + const group: VertSeg[] = [clusterSegs[i]!] + let j = i + 1 + while ( + j < clusterSegs.length && + curEnd >= clusterSegs[j]!.start - tolerance + ) { + curEnd = Math.max(curEnd, clusterSegs[j]!.end) + group.push(clusterSegs[j]!) + j++ + } + if (group.length > 1) { + const y0 = Math.min(...group.map((g) => g.start)) + const y1 = Math.max(...group.map((g) => g.end)) + let sumX = 0 + let c = 0 + for (const g of group) { + const pts = traces[g.mspPairId]!.tracePath + const a = pts[g.segIdx]! + const b = pts[g.segIdx + 1]! + sumX += a.x + b.x + c += 2 + } + const x = sumX / c + const byPath = new Map() + for (const g of group) { + const arr = byPath.get(g.mspPairId) ?? [] + arr.push(g.segIdx) + byPath.set(g.mspPairId, arr) + } + for (const [msp, idxs] of byPath) { + idxs.sort((a, b) => a - b) + const chains: number[][] = [] + let chain = [idxs[0]!] + for (let k = 1; k < idxs.length; k++) { + if (idxs[k] === idxs[k - 1]! + 1) { + chain.push(idxs[k]!) + } else { + chains.push(chain) + chain = [idxs[k]!] + } + } + chains.push(chain) + for (const ch of chains) { + ops.push({ + mspPairId: msp, + segFirst: ch[0]!, + segLast: ch[ch.length - 1]!, + y0, + y1, + x, + }) + } + } + } + i = j + } + } + } + return ops +} + +function applyHorizontalMergeOps( + traces: Record, + ops: HMergeOp[], +) { + ops.sort((a, b) => { + if (a.mspPairId !== b.mspPairId) { + return a.mspPairId.localeCompare(b.mspPairId) + } + return b.segFirst - a.segFirst + }) + for (const op of ops) { + const path = traces[op.mspPairId]!.tracePath + const prev = op.segFirst > 0 ? path[op.segFirst - 1]! : null + const next = op.segLast + 2 < path.length ? path[op.segLast + 2]! : null + const [p0, p1] = orderHorizontalEndpoints(prev, next, op.x0, op.x1, op.y) + traces[op.mspPairId]!.tracePath = spliceSegmentChain( + path, + op.segFirst, + op.segLast, + p0, + p1, + ) + } +} + +function applyVerticalMergeOps( + traces: Record, + ops: VMergeOp[], +) { + ops.sort((a, b) => { + if (a.mspPairId !== b.mspPairId) { + return a.mspPairId.localeCompare(b.mspPairId) + } + return b.segFirst - a.segFirst + }) + for (const op of ops) { + const path = traces[op.mspPairId]!.tracePath + const prev = op.segFirst > 0 ? path[op.segFirst - 1]! : null + const next = op.segLast + 2 < path.length ? path[op.segLast + 2]! : null + const [p0, p1] = orderVerticalEndpoints(prev, next, op.y0, op.y1, op.x) + traces[op.mspPairId]!.tracePath = spliceSegmentChain( + path, + op.segFirst, + op.segLast, + p0, + p1, + ) + } +} + +function mergeTraceMapInPlace( + traces: Record, + tolerance: number, +) { + const hOps = collectHorizontalMergeOps(traces, tolerance) + applyHorizontalMergeOps(traces, hOps) + const vOps = collectVerticalMergeOps(traces, tolerance) + applyVerticalMergeOps(traces, vOps) +} + +/** + * Merges nearly collinear orthogonal segments on the same net (same + * globalConnNetId) after overlap shifts are final. + */ +export class SameNetTraceLineMergeSolver extends BaseSolver { + inputProblem: InputProblem + correctedTraceMap: Record + tolerance: number + + mergedTraceMap: Record = {} + + constructor(params: { + inputProblem: InputProblem + correctedTraceMap: Record + tolerance?: number + }) { + super() + this.inputProblem = params.inputProblem + this.correctedTraceMap = params.correctedTraceMap + this.tolerance = params.tolerance ?? 0.01 + this.mergedTraceMap = deepCloneTraceMap(params.correctedTraceMap) + } + + override getConstructorParams(): ConstructorParameters< + typeof SameNetTraceLineMergeSolver + >[0] { + return { + inputProblem: this.inputProblem, + correctedTraceMap: { ...this.mergedTraceMap }, + tolerance: this.tolerance, + } + } + + override _step() { + mergeTraceMapInPlace(this.mergedTraceMap, this.tolerance) + this.solved = true + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem, { + chipAlpha: 0.15, + connectionAlpha: 0.15, + }) + for (const trace of Object.values(this.mergedTraceMap)) { + graphics.lines!.push({ + points: trace.tracePath, + strokeColor: "teal", + }) + } + return graphics + } +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index c9d5a995..e4699807 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -6,20 +6,21 @@ import type { GraphicsObject } from "graphics-debug" import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" import type { InputProblem } from "lib/types/InputProblem" +import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" import { MspConnectionPairSolver } from "../MspConnectionPairSolver/MspConnectionPairSolver" +import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { SameNetTraceLineMergeSolver } from "../SameNetTraceLineMergeSolver/SameNetTraceLineMergeSolver" import { SchematicTraceLinesSolver, type SolvedTracePath, } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" -import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlapShiftSolver" -import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" -import { visualizeInputProblem } from "./visualizeInputProblem" +import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" +import type { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" import { TraceLabelOverlapAvoidanceSolver } from "../TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver" +import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlapShiftSolver" import { correctPinsInsideChips } from "./correctPinsInsideChip" import { expandChipsToFitPins } from "./expandChipsToFitPins" -import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" -import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" -import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" +import { visualizeInputProblem } from "./visualizeInputProblem" type PipelineStep BaseSolver> = { solverName: string @@ -65,6 +66,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { schematicTraceLinesSolver?: SchematicTraceLinesSolver longDistancePairSolver?: LongDistancePairSolver traceOverlapShiftSolver?: TraceOverlapShiftSolver + sameNetTraceLineMergeSolver?: SameNetTraceLineMergeSolver netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver @@ -83,7 +85,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { MspConnectionPairSolver, () => [{ inputProblem: this.inputProblem }], { - onSolved: (mspSolver) => {}, + onSolved: (_mspSolver) => {}, }, ), // definePipelineStep( @@ -125,7 +127,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ], { - onSolved: (schematicTraceLinesSolver) => {}, + onSolved: (_schematicTraceLinesSolver) => {}, }, ), definePipelineStep( @@ -135,7 +137,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { { inputProblem: this.inputProblem, inputTracePaths: - this.longDistancePairSolver?.getOutput().allTracesMerged!, + this.longDistancePairSolver!.getOutput().allTracesMerged, globalConnMap: this.mspConnectionPairSolver!.globalConnMap, }, ], @@ -143,19 +145,25 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep( + "sameNetTraceLineMergeSolver", + SameNetTraceLineMergeSolver, + () => [ + // biome-ignore format: keep merge solver ctor args on one line for pipeline grep checks + { + inputProblem: this.inputProblem, + correctedTraceMap: { ...this.traceOverlapShiftSolver!.correctedTraceMap }, + tolerance: 0.01, + }, + ], + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, () => [ { inputProblem: this.inputProblem, - inputTraceMap: - this.traceOverlapShiftSolver?.correctedTraceMap ?? - Object.fromEntries( - this.longDistancePairSolver!.getOutput().allTracesMerged.map( - (p) => [p.mspPairId, p], - ), - ), + inputTraceMap: this.sameNetTraceLineMergeSolver!.mergedTraceMap, }, ], { @@ -168,13 +176,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { "traceLabelOverlapAvoidanceSolver", TraceLabelOverlapAvoidanceSolver, (instance) => { - const traceMap = - instance.traceOverlapShiftSolver?.correctedTraceMap ?? - Object.fromEntries( - instance - .longDistancePairSolver!.getOutput() - .allTracesMerged.map((p) => [p.mspPairId, p]), - ) + const traceMap = instance.sameNetTraceLineMergeSolver!.mergedTraceMap const traces = Object.values(traceMap) const netLabelPlacements = instance.netLabelPlacementSolver!.netLabelPlacements @@ -284,7 +286,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { } const constructorParams = pipelineStepDef.getConstructorParams(this) - // @ts-ignore + // @ts-expect-error this.activeSubSolver = new pipelineStepDef.solverClass(...constructorParams) ;(this as any)[pipelineStepDef.solverName] = this.activeSubSolver this.timeSpentOnPhase[pipelineStepDef.solverName] = 0 diff --git a/site/issue-34-input.ts b/site/issue-34-input.ts new file mode 100644 index 00000000..1f6ce196 --- /dev/null +++ b/site/issue-34-input.ts @@ -0,0 +1,41 @@ +import type { InputProblem } from "lib/types/InputProblem" + +/** + * Issue #34 repro + integration test — two ICs, VCC/GND direct ties. Orthogonal + * routes use a shared horizontal alley (y≈±0.6) so VCC and GND traces run + * parallel between the chips after overlap shifting. + */ +export const issue34InputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: -2.5, y: 0 }, + width: 1.6, + height: 0.8, + pins: [ + { pinId: "U1.1", x: -3.3, y: 0.15 }, + { pinId: "U1.2", x: -3.3, y: -0.15 }, + ], + }, + { + chipId: "U2", + center: { x: 2.5, y: 0 }, + width: 1.6, + height: 0.8, + pins: [ + { pinId: "U2.1", x: 1.7, y: 0.15 }, + { pinId: "U2.2", x: 1.7, y: -0.15 }, + ], + }, + ], + directConnections: [ + { pinIds: ["U1.1", "U2.1"], netId: "VCC" }, + { pinIds: ["U1.2", "U2.2"], netId: "GND" }, + ], + netConnections: [], + availableNetLabelOrientations: { + VCC: ["y+", "y-", "x+", "x-"], + GND: ["y+", "y-", "x+", "x-"], + }, + maxMspPairDistance: 20, +} diff --git a/site/issue-34-repro.page.tsx b/site/issue-34-repro.page.tsx new file mode 100644 index 00000000..27f3ef8c --- /dev/null +++ b/site/issue-34-repro.page.tsx @@ -0,0 +1,306 @@ +import type { GraphicsObject } from "graphics-debug" +import { getBounds, getSvgFromGraphicsObject } from "graphics-debug" +import { InteractiveGraphics } from "graphics-debug/react" +import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" +import { useMemo } from "react" +import { applyToPoint, compose, scale, translate } from "transformation-matrix" +import { issue34InputProblem } from "./issue-34-input" + +const SVG_SIZE = 640 +const SVG_PADDING = 40 + +/** Zoom: horizontal bus area between U1 and U2 (VCC / GND alleys). */ +const TRACE_FOCUS_CLIP = { + minX: -0.35, + maxX: 1.35, + minY: -0.72, + maxY: 0.72, +} as const + +function projectionMatrix( + bounds: { minX: number; maxX: number; minY: number; maxY: number }, + svgWidth: number, + svgHeight: number, +) { + const width = bounds.maxX - bounds.minX || 1 + const height = bounds.maxY - bounds.minY || 1 + const scaleFactor = Math.min( + (svgWidth - 2 * SVG_PADDING) / width, + (svgHeight - 2 * SVG_PADDING) / height, + ) + return compose( + translate(svgWidth / 2, svgHeight / 2), + scale(scaleFactor, -scaleFactor), + translate(-(bounds.minX + width / 2), -(bounds.minY + height / 2)), + ) +} + +function pixelViewBoxForSchematicClip( + graphics: GraphicsObject, + clip: { minX: number; maxX: number; minY: number; maxY: number }, +) { + const fullBounds = getBounds(graphics) + const M = projectionMatrix(fullBounds, SVG_SIZE, SVG_SIZE) + const corners = [ + { x: clip.minX, y: clip.minY }, + { x: clip.maxX, y: clip.minY }, + { x: clip.maxX, y: clip.maxY }, + { x: clip.minX, y: clip.maxY }, + ] + let minPxX = Infinity + let minPxY = Infinity + let maxPxX = -Infinity + let maxPxY = -Infinity + for (const c of corners) { + const p = applyToPoint(M, c) + minPxX = Math.min(minPxX, p.x) + maxPxX = Math.max(maxPxX, p.x) + minPxY = Math.min(minPxY, p.y) + maxPxY = Math.max(maxPxY, p.y) + } + const pad = 14 + return { + x: minPxX - pad, + y: minPxY - pad, + w: maxPxX - minPxX + 2 * pad, + h: maxPxY - minPxY + 2 * pad, + } +} + +function countTraceSegments( + traceMap: Record, +): number { + let n = 0 + for (const t of Object.values(traceMap)) { + n += Math.max(0, t.tracePath.length - 1) + } + return n +} + +/** Long horizontal bus segments in the zoom band (VCC + GND each contribute one). */ +function countParallelBusSegmentsInClip( + traceMap: Record, +): number { + let n = 0 + for (const t of Object.values(traceMap)) { + const pts = t.tracePath + for (let i = 0; i < pts.length - 1; i++) { + const a = pts[i]! + const b = pts[i + 1]! + if (Math.abs(a.y - b.y) > 1e-9) continue + const y = (a.y + b.y) / 2 + if (Math.abs(y) < 0.35 || Math.abs(y) > 0.75) continue + const x0 = Math.min(a.x, b.x) + const x1 = Math.max(a.x, b.x) + if (x1 < TRACE_FOCUS_CLIP.minX || x0 > TRACE_FOCUS_CLIP.maxX) continue + if (x1 - x0 < 0.8) continue + n++ + } + } + return n +} + +function SvgZoomInset(props: { + fullSvgMarkup: string + clip: { x: number; y: number; w: number; h: number } + caption: string +}) { + const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(props.fullSvgMarkup)}` + return ( +
+
+ {props.caption} +
+
+ + + +
+
+ ) +} + +export default function Issue34ReproPage() { + const { + beforeGraphics, + afterGraphics, + beforeSvg, + afterSvg, + beforeClip, + afterClip, + beforeTotalSegments, + afterTotalSegments, + beforeBusCount, + afterBusCount, + } = useMemo(() => { + const before = new SchematicTracePipelineSolver(issue34InputProblem) + before.solveUntilPhase("sameNetTraceLineMergeSolver") + const beforeMap = before.traceOverlapShiftSolver!.correctedTraceMap + const beforeGraphicsInner = before.visualize() as GraphicsObject + + const after = new SchematicTracePipelineSolver(issue34InputProblem) + after.solve() + const mergedMap = after.sameNetTraceLineMergeSolver!.mergedTraceMap + const afterGraphicsInner = after.visualize() as GraphicsObject + + const svgOpts = { backgroundColor: "white" as const } + const beforeSvgInner = getSvgFromGraphicsObject( + beforeGraphicsInner, + svgOpts, + ) + const afterSvgInner = getSvgFromGraphicsObject(afterGraphicsInner, svgOpts) + + return { + beforeGraphics: beforeGraphicsInner, + afterGraphics: afterGraphicsInner, + beforeSvg: beforeSvgInner, + afterSvg: afterSvgInner, + beforeClip: pixelViewBoxForSchematicClip( + beforeGraphicsInner, + TRACE_FOCUS_CLIP, + ), + afterClip: pixelViewBoxForSchematicClip( + afterGraphicsInner, + TRACE_FOCUS_CLIP, + ), + beforeTotalSegments: countTraceSegments(beforeMap), + afterTotalSegments: countTraceSegments(mergedMap), + beforeBusCount: countParallelBusSegmentsInClip(beforeMap), + afterBusCount: countParallelBusSegmentsInClip(mergedMap), + } + }, []) + + return ( +
+

Issue #34 — same-net trace merge

+

+ Two ICs with VCC on the upper pins and GND on + the lower pins. Orthogonal routing forms two long, nearly stacked + horizontal buses between the chips; overlap shifting brings them close + in Y. SameNetTraceLineMergeSolver then cleans same-net + collinear fragments (see full pipeline on the right). +

+
+
+

+ BEFORE — pre-merge (overlap-shift output only) +

+

+ Pipeline stopped before SameNetTraceLineMergeSolver. + Purple / green polylines are the routed traces; inset highlights the + parallel horizontal buses. +

+
+ +
+ +
+
+ Long horizontal buses in inset band:{" "} + {beforeBusCount} +
+
+ Orthogonal segments (sum over paths, pre-merge):{" "} + {beforeTotalSegments} +
+
+
+ +
+

AFTER — full pipeline (solved)

+
+
+ Uses afterSolver.solve() then{" "} + afterSolver.visualize() (same as{" "} + GenericSolverDebugger) so chips, pins, and all trace + layers render together. +
+
+ Teal segments include output from{" "} + SameNetTraceLineMergeSolver; other colors come from + earlier pipeline stages and labels. +
+
+
+ +
+ +
+
+ Long horizontal buses in inset band:{" "} + {afterBusCount} +
+
+ Orthogonal segments (sum over paths, merged map):{" "} + {afterTotalSegments} +
+
+
+
+
+ ) +} diff --git a/tests/examples/__snapshots__/example-issue34.snap.svg b/tests/examples/__snapshots__/example-issue34.snap.svg new file mode 100644 index 00000000..54050aeb --- /dev/null +++ b/tests/examples/__snapshots__/example-issue34.snap.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/examples/__snapshots__/example01.snap.svg b/tests/examples/__snapshots__/example01.snap.svg index 293bf05a..e0d6a45f 100644 --- a/tests/examples/__snapshots__/example01.snap.svg +++ b/tests/examples/__snapshots__/example01.snap.svg @@ -47,7 +47,7 @@ y-" data-x="-4" data-y="-0.5" cx="67.72277227722776" cy="356.03960396039605" r=" - + @@ -71,10 +71,10 @@ y-" data-x="-4" data-y="-0.5" cx="67.72277227722776" cy="356.03960396039605" r=" - + - + @@ -92,7 +92,7 @@ y-" data-x="-4" data-y="-0.5" cx="67.72277227722776" cy="356.03960396039605" r=" - + - + @@ -152,25 +152,25 @@ x+" data-x="1" data-y="0.1" cx="500.20151295522464" cy="324.189420823755" r="3" - + - + - + - + - + - + - + @@ -194,7 +194,7 @@ x+" data-x="1" data-y="0.1" cx="500.20151295522464" cy="324.189420823755" r="3" - + diff --git a/tests/examples/__snapshots__/example09.snap.svg b/tests/examples/__snapshots__/example09.snap.svg index 1a3df33b..03292a61 100644 --- a/tests/examples/__snapshots__/example09.snap.svg +++ b/tests/examples/__snapshots__/example09.snap.svg @@ -167,34 +167,34 @@ x+" data-x="4.5512907" data-y="1.9997267500000007" cx="574.7555654852287" cy="18 - + - + - + - + - + - + - + - + - + @@ -266,34 +266,34 @@ x+" data-x="4.5512907" data-y="1.9997267500000007" cx="574.7555654852287" cy="18 - + - + - + - + - + - + - + - + - + - + @@ -341,34 +341,34 @@ x+" data-x="4.5512907" data-y="1.9997267500000007" cx="574.7555654852287" cy="18 - + - + - + - + - + - + - + - + - + diff --git a/tests/examples/__snapshots__/example11.snap.svg b/tests/examples/__snapshots__/example11.snap.svg index c7140949..819809e4 100644 --- a/tests/examples/__snapshots__/example11.snap.svg +++ b/tests/examples/__snapshots__/example11.snap.svg @@ -49,7 +49,7 @@ x+" data-x="1.48" data-y="-0.665" cx="536.2544169611307" cy="427.0671378091873" - + @@ -67,7 +67,7 @@ x+" data-x="1.48" data-y="-0.665" cx="536.2544169611307" cy="427.0671378091873" - + @@ -97,7 +97,7 @@ x+" data-x="1.48" data-y="-0.665" cx="536.2544169611307" cy="427.0671378091873" - + diff --git a/tests/examples/__snapshots__/example12.snap.svg b/tests/examples/__snapshots__/example12.snap.svg index 164322ef..e69b97d4 100644 --- a/tests/examples/__snapshots__/example12.snap.svg +++ b/tests/examples/__snapshots__/example12.snap.svg @@ -2,88 +2,88 @@ +x+" data-x="1.1" data-y="0.2" cx="288.33702882483374" cy="202.3528526506753" r="3" fill="hsl(218, 100%, 50%, 0.8)" /> +x+" data-x="1.1" data-y="0" cx="288.33702882483374" cy="224.92894618020563" r="3" fill="hsl(219, 100%, 50%, 0.8)" /> +x+" data-x="1.1" data-y="-0.2" cx="288.33702882483374" cy="247.50503970973597" r="3" fill="hsl(220, 100%, 50%, 0.8)" /> +x-" data-x="0.5499999999999996" data-y="-1.7944553499999996" cx="226.25277161862525" cy="427.4879052610361" r="3" fill="hsl(226, 100%, 50%, 0.8)" /> +x+" data-x="1.6499999999999997" data-y="-1.7944553499999996" cx="350.4212860310421" cy="427.4879052610361" r="3" fill="hsl(227, 100%, 50%, 0.8)" /> +x-" data-x="2.3099999999999996" data-y="0.01999999999999985" cx="424.9223946784922" cy="222.67133682725262" r="3" fill="hsl(121, 100%, 50%, 0.8)" /> +x+" data-x="3.41" data-y="0.01999999999999985" cx="549.090909090909" cy="222.67133682725262" r="3" fill="hsl(122, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +