diff --git a/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts new file mode 100644 index 00000000..cbe3ee66 --- /dev/null +++ b/lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts @@ -0,0 +1,160 @@ +import type { Point } from "@tscircuit/math-utils" +import { BaseSolver } from "../BaseSolver/BaseSolver" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +export interface SameNetTraceMergeSolverParams { + traces: SolvedTracePath[] + /** + * Maximum distance between two trace endpoints to consider them mergeable. + * Default: 0.12 units. + */ + maxEndpointGap?: number +} + +function dist(a: Point, b: Point): number { + return Math.hypot(a.x - b.x, a.y - b.y) +} + +/** Net key for grouping – prefer user-defined net id, fall back to connectivity ids */ +function netKey(trace: SolvedTracePath): string { + return trace.userNetId ?? trace.globalConnNetId ?? trace.dcConnNetId +} + +/** Remove exact duplicate consecutive points */ +function dedupe(pts: Point[]): Point[] { + const out: Point[] = [] + for (const p of pts) { + const prev = out[out.length - 1] + if ( + !prev || + Math.abs(prev.x - p.x) > 1e-9 || + Math.abs(prev.y - p.y) > 1e-9 + ) { + out.push(p) + } + } + return out +} + +/** + * Try to merge two same-net traces by finding the closest endpoint pair. + * If the join requires a direction change, a single right-angle bridge is inserted. + * Returns the merged SolvedTracePath or null if the gap exceeds maxGap. + */ +function tryMerge( + a: SolvedTracePath, + b: SolvedTracePath, + maxGap: number, +): SolvedTracePath | null { + const pa = a.tracePath + const pb = b.tracePath + if (pa.length === 0 || pb.length === 0) return null + + const aS = pa[0]! + const aE = pa[pa.length - 1]! + const bS = pb[0]! + const bE = pb[pb.length - 1]! + + const options = [ + { d: dist(aE, bS), revA: false, revB: false }, + { d: dist(aE, bE), revA: false, revB: true }, + { d: dist(aS, bS), revA: true, revB: false }, + { d: dist(aS, bE), revA: true, revB: true }, + ] + const best = options.reduce((p, c) => (c.d < p.d ? c : p)) + if (best.d > maxGap) return null + + const ordA = best.revA ? [...pa].reverse() : [...pa] + const ordB = best.revB ? [...pb].reverse() : [...pb] + + const from = ordA[ordA.length - 1]! + const to = ordB[0]! + + // Insert a single L-shaped bridge if the connection is not axis-aligned + const bridge: Point[] = + Math.abs(from.x - to.x) > 1e-9 && Math.abs(from.y - to.y) > 1e-9 + ? [{ x: to.x, y: from.y }] + : [] + + return { + ...a, + mspPairId: `merged:${a.mspPairId}+${b.mspPairId}`, + tracePath: dedupe([...ordA, ...bridge, ...ordB]), + mspConnectionPairIds: [ + ...a.mspConnectionPairIds, + ...b.mspConnectionPairIds, + ], + pinIds: [...a.pinIds, ...b.pinIds], + } +} + +/** + * SameNetTraceMergeSolver — pipeline phase that joins trace segments belonging + * to the same electrical net when their endpoints are within `maxEndpointGap` + * units of each other. + * + * Runs iteratively until no more merges are possible (or the solver is marked + * solved/failed by the base class iteration guard). + */ +export class SameNetTraceMergeSolver extends BaseSolver { + readonly inputTraces: SolvedTracePath[] + outputTraces: SolvedTracePath[] + readonly maxEndpointGap: number + /** Total number of merges performed across all _step() calls */ + mergeCount = 0 + + constructor({ + traces, + maxEndpointGap = 0.12, + }: SameNetTraceMergeSolverParams) { + super() + this.inputTraces = [...traces] + this.outputTraces = [...traces] + this.maxEndpointGap = maxEndpointGap + } + + getOutput(): { traces: SolvedTracePath[] } { + return { traces: this.outputTraces } + } + + override _step(): void { + // Group current traces by net + const byNet = new Map() + for (const trace of this.outputTraces) { + const k = netKey(trace) + const group = byNet.get(k) + if (group) group.push(trace) + else byNet.set(k, [trace]) + } + + let merged = false + + for (const group of byNet.values()) { + if (group.length < 2) continue + + // O(n²) scan – groups are typically small + outer: for (let i = 0; i < group.length; i++) { + for (let j = i + 1; j < group.length; j++) { + const joined = tryMerge(group[i]!, group[j]!, this.maxEndpointGap) + if (joined) { + // Replace the two original traces with the merged one + this.outputTraces = this.outputTraces.filter( + (t) => + t.mspPairId !== group[i]!.mspPairId && + t.mspPairId !== group[j]!.mspPairId, + ) + this.outputTraces.push(joined) + this.mergeCount++ + merged = true + break outer // Restart the scan for this net + } + } + } + } + + if (!merged) { + // No more mergeable pairs – we're done + this.solved = true + } + } +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c..4ea4788e 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -21,6 +21,7 @@ import { expandChipsToFitPins } from "./expandChipsToFitPins" import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" +import { SameNetTraceMergeSolver } from "../SameNetTraceMergeSolver/SameNetTraceMergeSolver" import { Example28Solver } from "../Example28Solver/Example28Solver" import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver" import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver" @@ -75,6 +76,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver traceCleanupSolver?: TraceCleanupSolver + sameNetTraceMergeSolver?: SameNetTraceMergeSolver example28Solver?: Example28Solver availableNetOrientationSolver?: AvailableNetOrientationSolver vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver @@ -217,11 +219,24 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ] }), + definePipelineStep( + "sameNetTraceMergeSolver", + SameNetTraceMergeSolver, + (instance) => { + const traces = + instance.sameNetTraceMergeSolver?.getOutput().traces ?? + instance.traceCleanupSolver?.getOutput().traces ?? + instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces + return [{ traces }] + }, + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, (instance) => { const traces = + instance.sameNetTraceMergeSolver?.getOutput().traces ?? + instance.sameNetTraceMergeSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces @@ -237,6 +252,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { ), definePipelineStep("example28Solver", Example28Solver, (instance) => { const traces = + instance.sameNetTraceMergeSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces diff --git a/tests/examples/__snapshots__/example01.snap.svg b/tests/examples/__snapshots__/example01.snap.svg index 2614ba80..e185ca90 100644 --- a/tests/examples/__snapshots__/example01.snap.svg +++ b/tests/examples/__snapshots__/example01.snap.svg @@ -41,16 +41,16 @@ y+" data-x="-4" data-y="0.5" cx="67.72277227722776" cy="245.14851485148515" r="3 y-" data-x="-4" data-y="-0.5" cx="67.72277227722776" cy="356.03960396039605" r="3" fill="hsl(3, 100%, 50%, 0.8)" /> - + - + - + + + + @@ -74,10 +74,7 @@ orientation: y-" data-x="-1.4" data-y="-0.7" cx="356.0396039603961" cy="378.2178 - - - - + @@ -90,18 +87,19 @@ orientation: y-" data-x="-1.4" data-y="-0.7" cx="356.0396039603961" cy="378.2178 +globalConnNetId: connectivity_net0" data-x="-1.1" data-y="0.42500000000000016" x="378.21782178217825" y="228.51485148514848" width="22.178217821782198" height="49.9009900990099" fill="#ef444466" stroke="#ef4444" stroke-width="0.009017857142857143" /> +globalConnNetId: connectivity_net1" data-x="-1.5" data-y="0" x="320" y="289.5049504950495" width="49.90099009900996" height="22.17821782178214" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009017857142857143" /> + + + +globalConnNetId: connectivity_net2" data-x="-4" data-y="-0.726" x="56.63366336633669" y="356.1504950495049" width="22.17821782178214" height="49.90099009900996" fill="#00000066" stroke="#000000" stroke-width="0.009017857142857143" /> - + - + - + - + + + + + + + + + + + + + + + + @@ -156,63 +167,70 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53 - + - + - + - + - + - + - + - + - + - + - + - + - + +globalConnNetId: connectivity_net0" data-x="-2.301" data-y="0.07599999999999998" x="212.35868019581574" y="307.17783323638616" width="16.92695282325252" height="38.085643852318185" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" /> + + + + + + +globalConnNetId: connectivity_net1" data-x="-3.0434765500000003" data-y="-0.22600000000000023" x="149.51935252470935" y="332.7375319994975" width="16.926952823252492" height="38.08564385231813" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net1" data-x="1.9148566499999995" data-y="-1.2284186000000008" x="569.1667133165424" y="417.5769937562517" width="16.926952823252577" height="38.08564385231813" fill="#00000066" stroke="#000000" stroke-width="0.011815475714285715" /> +globalConnNetId: connectivity_net2" data-x="1.4571549750000001" data-y="0.5249999999999997" x="530.4292400172993" y="269.1768241481843" width="16.926952823252464" height="38.08564385231813" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" /> - + - + - + - + - + - + - + - + + + + + + + @@ -193,37 +191,37 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492 - + - + - + - + - + - + - + - + - + - + - + @@ -251,43 +249,43 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492 +globalConnNetId: connectivity_net3" data-x="1.7009999999999998" data-y="0.474" x="394.33081834010443" y="307.0644225188625" width="13.000580383052807" height="29.251305861868843" fill="#00000066" stroke="#000000" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net3" data-x="-2.125" data-y="-2.2249999999999996" x="145.6297156123041" y="482.5072547881602" width="13.000580383052835" height="29.251305861868843" fill="#00000066" stroke="#000000" stroke-width="0.015383928571428571" /> + + + +globalConnNetId: connectivity_net4" data-x="1.475" data-y="0.225" x="379.6401625072548" y="323.25014509576323" width="13.000580383052807" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net4" data-x="-1.301" data-y="-0.8490000000000001" x="199.19210679048172" y="393.0632617527569" width="13.000580383052807" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net2" data-x="-2.125" data-y="0.225" x="145.6297156123041" y="323.25014509576323" width="13.000580383052835" height="29.251305861868843" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.015383928571428571" /> + + + +globalConnNetId: connectivity_net0" data-x="-3.75" data-y="2.225" x="39.99999999999997" y="193.24434126523508" width="13.000580383052835" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net0" data-x="2.2990000000000004" data-y="0.276" x="433.20255368543235" y="319.93499709808475" width="13.000580383052863" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> +globalConnNetId: connectivity_net1" data-x="1.96375" data-y="3.225" x="411.41033081834007" y="128.241439349971" width="13.000580383052807" height="29.25130586186887" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.015383928571428571" /> - + - + - + - + - + - + - + - + + + + @@ -155,9 +150,6 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 - - - @@ -168,10 +160,10 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 - + - + @@ -196,43 +188,39 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 +globalConnNetId: connectivity_net4" data-x="1.3010000000000002" data-y="-0.7260000000000001" x="419.16307692307697" y="335.1630769230769" 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.4260000000000002" data-y="-1.2944553500000002" x="419.16307692307697" y="394.9069224615385" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -776,154 +809,171 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354 - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +globalConnNetId: connectivity_net0" data-x="1.9525000000000015" data-y="6.9300000000000015" x="509.74900924702774" y="40.00000000000006" width="9.863496257155475" height="22.192866578599705" 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_net6" data-x="-6.415" data-y="3.9943333333333353" x="97.08498458828723" y="184.7796858946132" width="9.863496257155418" 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="-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="-6.415" data-y="2.4423333333333357" x="97.08498458828723" y="261.32041685013934" width="9.863496257155418" height="22.192866578599762" 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" /> - + - + - + - + - + + + + @@ -136,19 +134,19 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" - + - + - + - + - + @@ -167,28 +165,27 @@ 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.7009999999999998" data-y="-0.224" x="479.9672460962966" y="304.24341569495203" width="17.90520943755456" height="40.286721234497634" 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" />