From 2958c60803e427611b9f0dabdaaee07eec1b4b3d Mon Sep 17 00:00:00 2001
From: vinaykrsinghal-stage
<253648342+vinaykrsinghal-stage@users.noreply.github.com>
Date: Sat, 16 May 2026 18:01:21 +0530
Subject: [PATCH] Coalesce same-net trace segments
---
.../TraceCleanupSolver/TraceCleanupSolver.ts | 20 +-
.../coalesceSameNetTraces.ts | 427 ++++++++++++++++++
.../examples/__snapshots__/example02.snap.svg | 32 +-
.../examples/__snapshots__/example13.snap.svg | 56 +--
.../examples/__snapshots__/example14.snap.svg | 54 +--
.../examples/__snapshots__/example15.snap.svg | 78 ++--
.../examples/__snapshots__/example18.snap.svg | 32 +-
.../examples/__snapshots__/example19.snap.svg | 16 +-
.../examples/__snapshots__/example21.snap.svg | 50 +-
.../examples/__snapshots__/example29.snap.svg | 182 +++-----
.../coalesceSameNetTraces.test.ts | 107 +++++
11 files changed, 735 insertions(+), 319 deletions(-)
create mode 100644 lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts
create mode 100644 tests/solvers/TraceCleanupSolver/coalesceSameNetTraces.test.ts
diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
index e9bac7ca..c0ccf138 100644
--- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
+++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
@@ -20,6 +20,7 @@ interface TraceCleanupSolverInput {
import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
import { is4PointRectangle } from "./is4PointRectangle"
+import { coalesceSameNetTraces } from "./coalesceSameNetTraces"
/**
* Represents the different stages or steps within the trace cleanup pipeline.
@@ -27,6 +28,7 @@ import { is4PointRectangle } from "./is4PointRectangle"
type PipelineStep =
| "minimizing_turns"
| "balancing_l_shapes"
+ | "coalescing_same_net_traces"
| "untangling_traces"
/**
@@ -34,7 +36,8 @@ type PipelineStep =
* It operates in a multi-step pipeline:
* 1. **Untangling Traces**: It first attempts to untangle any overlapping or highly convoluted traces using a sub-solver.
* 2. **Minimizing Turns**: After untangling, it iterates through each trace to minimize the number of turns, simplifying their paths.
- * 3. **Balancing L-Shapes**: Finally, it balances L-shaped trace segments to create more visually appealing and consistent layouts.
+ * 3. **Balancing L-Shapes**: It balances L-shaped trace segments to create more visually appealing and consistent layouts.
+ * 4. **Coalescing Same-Net Traces**: Finally, it snaps close, overlapping same-net internal segments onto shared runs.
* The solver processes traces one by one, applying these cleanup steps sequentially to refine the overall trace layout.
*/
export class TraceCleanupSolver extends BaseSolver {
@@ -84,6 +87,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "balancing_l_shapes":
this._runBalanceLShapesStep()
break
+ case "coalescing_same_net_traces":
+ this._runCoalesceSameNetTracesStep()
+ break
}
}
@@ -108,13 +114,23 @@ export class TraceCleanupSolver extends BaseSolver {
private _runBalanceLShapesStep() {
if (this.traceIdQueue.length === 0) {
- this.solved = true
+ this.pipelineStep = "coalescing_same_net_traces"
return
}
this._processTrace("balancing_l_shapes")
}
+ private _runCoalesceSameNetTracesStep() {
+ const { traces, coalescedSegmentCount } = coalesceSameNetTraces(
+ this.outputTraces,
+ )
+ this.outputTraces = traces
+ this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
+ this.stats.coalescedSameNetSegmentCount = coalescedSegmentCount
+ this.solved = true
+ }
+
private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") {
const targetMspConnectionPairId = this.traceIdQueue.shift()!
this.activeTraceId = targetMspConnectionPairId
diff --git a/lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts b/lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts
new file mode 100644
index 00000000..3d0bf251
--- /dev/null
+++ b/lib/solvers/TraceCleanupSolver/coalesceSameNetTraces.ts
@@ -0,0 +1,427 @@
+import type { Point } from "@tscircuit/math-utils"
+import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
+import {
+ isHorizontal,
+ isVertical,
+} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
+import { simplifyPath } from "./simplifyPath"
+
+const EPS = 1e-6
+
+type Orientation = "horizontal" | "vertical"
+
+interface SegmentRef {
+ traceIndex: number
+ segmentIndex: number
+ orientation: Orientation
+ coord: number
+ start: number
+ end: number
+ length: number
+ isEndpointSegment: boolean
+ isInternalSegment: boolean
+ globalConnNetId: string
+}
+
+export interface CoalesceSameNetTracesResult {
+ traces: SolvedTracePath[]
+ coalescedSegmentCount: number
+}
+
+const cloneTrace = (trace: SolvedTracePath): SolvedTracePath => ({
+ ...trace,
+ tracePath: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
+})
+
+const getSegmentRef = (
+ trace: SolvedTracePath,
+ traceIndex: number,
+ segmentIndex: number,
+): SegmentRef | null => {
+ const p1 = trace.tracePath[segmentIndex]
+ const p2 = trace.tracePath[segmentIndex + 1]
+ if (!p1 || !p2) return null
+
+ const isInternalSegment =
+ segmentIndex > 0 && segmentIndex < trace.tracePath.length - 2
+ const isEndpointSegment =
+ segmentIndex === 0 || segmentIndex === trace.tracePath.length - 2
+
+ if (isHorizontal(p1, p2, EPS)) {
+ const start = Math.min(p1.x, p2.x)
+ const end = Math.max(p1.x, p2.x)
+ return {
+ traceIndex,
+ segmentIndex,
+ orientation: "horizontal",
+ coord: p1.y,
+ start,
+ end,
+ length: end - start,
+ isEndpointSegment,
+ isInternalSegment,
+ globalConnNetId: trace.globalConnNetId,
+ }
+ }
+
+ if (isVertical(p1, p2, EPS)) {
+ const start = Math.min(p1.y, p2.y)
+ const end = Math.max(p1.y, p2.y)
+ return {
+ traceIndex,
+ segmentIndex,
+ orientation: "vertical",
+ coord: p1.x,
+ start,
+ end,
+ length: end - start,
+ isEndpointSegment,
+ isInternalSegment,
+ globalConnNetId: trace.globalConnNetId,
+ }
+ }
+
+ return null
+}
+
+const collectSegments = (traces: SolvedTracePath[]): SegmentRef[] => {
+ const segments: SegmentRef[] = []
+ traces.forEach((trace, traceIndex) => {
+ for (
+ let segmentIndex = 0;
+ segmentIndex < trace.tracePath.length - 1;
+ segmentIndex++
+ ) {
+ const segment = getSegmentRef(trace, traceIndex, segmentIndex)
+ if (segment && segment.length > EPS) {
+ segments.push(segment)
+ }
+ }
+ })
+ return segments
+}
+
+const getOverlapLength = (a: SegmentRef, b: SegmentRef): number =>
+ Math.min(a.end, b.end) - Math.max(a.start, b.start)
+
+const getOverlapRange = (a: SegmentRef, b: SegmentRef) => ({
+ start: Math.max(a.start, b.start),
+ end: Math.min(a.end, b.end),
+})
+
+const getPathWithSegmentCoord = (
+ path: Point[],
+ segmentIndex: number,
+ orientation: Orientation,
+ coord: number,
+): Point[] =>
+ path.map((point, index) => {
+ if (index !== segmentIndex && index !== segmentIndex + 1) {
+ return { x: point.x, y: point.y }
+ }
+ return orientation === "horizontal"
+ ? { x: point.x, y: coord }
+ : { x: coord, y: point.y }
+ })
+
+const getMajorCoord = (point: Point, orientation: Orientation) =>
+ orientation === "horizontal" ? point.x : point.y
+
+const withMajorCoord = (
+ point: Point,
+ orientation: Orientation,
+ coord: number,
+): Point =>
+ orientation === "horizontal"
+ ? { x: coord, y: point.y }
+ : { x: point.x, y: coord }
+
+const removeConsecutiveDuplicatePoints = (path: Point[]) => {
+ const deduped: Point[] = []
+ for (const point of path) {
+ if (
+ deduped.length === 0 ||
+ !samePoint(deduped[deduped.length - 1]!, point)
+ ) {
+ deduped.push(point)
+ }
+ }
+ return deduped
+}
+
+const getPathWithTrimmedEndpointOverlap = (
+ trace: SolvedTracePath,
+ segment: SegmentRef,
+ overlap: { start: number; end: number },
+): Point[] | null => {
+ if (!segment.isEndpointSegment || trace.tracePath.length <= 2) return null
+
+ const endpointIndex =
+ segment.segmentIndex === 0 ? 0 : segment.segmentIndex + 1
+ const neighborIndex = segment.segmentIndex === 0 ? 1 : segment.segmentIndex
+ const endpoint = trace.tracePath[endpointIndex]
+ const neighbor = trace.tracePath[neighborIndex]
+ if (!endpoint || !neighbor) return null
+
+ const endpointMajor = getMajorCoord(endpoint, segment.orientation)
+ const neighborMajor = getMajorCoord(neighbor, segment.orientation)
+ let newEndpointMajor: number | null = null
+
+ if (endpointMajor < neighborMajor && overlap.start <= endpointMajor + EPS) {
+ newEndpointMajor = Math.min(overlap.end, neighborMajor)
+ } else if (
+ endpointMajor > neighborMajor &&
+ overlap.end >= endpointMajor - EPS
+ ) {
+ newEndpointMajor = Math.max(overlap.start, neighborMajor)
+ }
+
+ if (newEndpointMajor === null) return null
+ if (Math.abs(newEndpointMajor - endpointMajor) < EPS) return null
+
+ const candidatePath = trace.tracePath.map((point, index) =>
+ index === endpointIndex
+ ? withMajorCoord(point, segment.orientation, newEndpointMajor)
+ : { x: point.x, y: point.y },
+ )
+ const dedupedPath = removeConsecutiveDuplicatePoints(candidatePath)
+ if (dedupedPath.length < 2) return null
+
+ return simplifyPath(dedupedPath)
+}
+
+const samePoint = (a: Point, b: Point) =>
+ Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS
+
+const pointOnSegment = (point: Point, a: Point, b: Point) => {
+ if (isHorizontal(a, b, EPS)) {
+ return (
+ Math.abs(point.y - a.y) < EPS &&
+ point.x >= Math.min(a.x, b.x) - EPS &&
+ point.x <= Math.max(a.x, b.x) + EPS
+ )
+ }
+ if (isVertical(a, b, EPS)) {
+ return (
+ Math.abs(point.x - a.x) < EPS &&
+ point.y >= Math.min(a.y, b.y) - EPS &&
+ point.y <= Math.max(a.y, b.y) + EPS
+ )
+ }
+ return false
+}
+
+const endpointOnlyTouch = (
+ point: Point,
+ a1: Point,
+ a2: Point,
+ b1: Point,
+ b2: Point,
+) =>
+ (samePoint(point, a1) || samePoint(point, a2)) &&
+ (samePoint(point, b1) || samePoint(point, b2))
+
+const axisAlignedSegmentsIntersect = (
+ a1: Point,
+ a2: Point,
+ b1: Point,
+ b2: Point,
+): boolean => {
+ const aHorizontal = isHorizontal(a1, a2, EPS)
+ const aVertical = isVertical(a1, a2, EPS)
+ const bHorizontal = isHorizontal(b1, b2, EPS)
+ const bVertical = isVertical(b1, b2, EPS)
+ if ((!aHorizontal && !aVertical) || (!bHorizontal && !bVertical)) return false
+
+ if (aHorizontal && bHorizontal) {
+ if (Math.abs(a1.y - b1.y) >= EPS) return false
+ return (
+ Math.min(Math.max(a1.x, a2.x), Math.max(b1.x, b2.x)) -
+ Math.max(Math.min(a1.x, a2.x), Math.min(b1.x, b2.x)) >
+ EPS
+ )
+ }
+
+ if (aVertical && bVertical) {
+ if (Math.abs(a1.x - b1.x) >= EPS) return false
+ return (
+ Math.min(Math.max(a1.y, a2.y), Math.max(b1.y, b2.y)) -
+ Math.max(Math.min(a1.y, a2.y), Math.min(b1.y, b2.y)) >
+ EPS
+ )
+ }
+
+ const horizontalStart = aHorizontal ? a1 : b1
+ const horizontalEnd = aHorizontal ? a2 : b2
+ const verticalStart = aVertical ? a1 : b1
+ const verticalEnd = aVertical ? a2 : b2
+ const intersection = { x: verticalStart.x, y: horizontalStart.y }
+
+ if (
+ pointOnSegment(intersection, horizontalStart, horizontalEnd) &&
+ pointOnSegment(intersection, verticalStart, verticalEnd)
+ ) {
+ return !endpointOnlyTouch(intersection, a1, a2, b1, b2)
+ }
+
+ return false
+}
+
+const pathCollidesWithDifferentNetTrace = (
+ path: Point[],
+ globalConnNetId: string,
+ otherTrace: SolvedTracePath,
+) => {
+ if (otherTrace.globalConnNetId === globalConnNetId) return false
+
+ for (let i = 0; i < path.length - 1; i++) {
+ for (let j = 0; j < otherTrace.tracePath.length - 1; j++) {
+ if (
+ axisAlignedSegmentsIntersect(
+ path[i]!,
+ path[i + 1]!,
+ otherTrace.tracePath[j]!,
+ otherTrace.tracePath[j + 1]!,
+ )
+ ) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+const isSafeTracePath = (
+ candidatePath: Point[],
+ candidateNetId: string,
+ traces: SolvedTracePath[],
+ candidateTraceIndex: number,
+) =>
+ traces.every((trace, traceIndex) => {
+ if (traceIndex === candidateTraceIndex) return true
+ return !pathCollidesWithDifferentNetTrace(
+ candidatePath,
+ candidateNetId,
+ trace,
+ )
+ })
+
+const pathsEqual = (a: Point[], b: Point[]) =>
+ a.length === b.length &&
+ a.every((point, index) => samePoint(point, b[index]!))
+
+export const coalesceSameNetTraces = (
+ traces: SolvedTracePath[],
+ opts: {
+ maxSnapDistance?: number
+ minOverlap?: number
+ maxPasses?: number
+ } = {},
+): CoalesceSameNetTracesResult => {
+ const maxSnapDistance = opts.maxSnapDistance ?? 0.25
+ const minOverlap = opts.minOverlap ?? 0.05
+ const maxPasses = opts.maxPasses ?? 20
+ const outputTraces = traces.map(cloneTrace)
+ let coalescedSegmentCount = 0
+
+ for (let pass = 0; pass < maxPasses; pass++) {
+ const segments = collectSegments(outputTraces)
+ let changedThisPass = false
+
+ for (let i = 0; i < segments.length; i++) {
+ const a = segments[i]!
+ for (let j = i + 1; j < segments.length; j++) {
+ const b = segments[j]!
+ if (a.traceIndex === b.traceIndex) continue
+ if (a.globalConnNetId !== b.globalConnNetId) continue
+ if (a.orientation !== b.orientation) continue
+
+ const coordDistance = Math.abs(a.coord - b.coord)
+ const overlapLength = getOverlapLength(a, b)
+ if (overlapLength < minOverlap) continue
+
+ if (coordDistance < EPS) {
+ const overlap = getOverlapRange(a, b)
+ const trimCandidates = [a, b]
+ .filter((segment) => segment.isEndpointSegment)
+ .sort((left, right) => left.length - right.length)
+
+ for (const moving of trimCandidates) {
+ const movingTrace = outputTraces[moving.traceIndex]!
+ const candidatePath = getPathWithTrimmedEndpointOverlap(
+ movingTrace,
+ moving,
+ overlap,
+ )
+ if (!candidatePath) continue
+ if (pathsEqual(candidatePath, movingTrace.tracePath)) continue
+ if (
+ !isSafeTracePath(
+ candidatePath,
+ movingTrace.globalConnNetId,
+ outputTraces,
+ moving.traceIndex,
+ )
+ ) {
+ continue
+ }
+
+ outputTraces[moving.traceIndex] = {
+ ...movingTrace,
+ tracePath: candidatePath,
+ }
+ coalescedSegmentCount++
+ changedThisPass = true
+ break
+ }
+
+ if (changedThisPass) break
+ continue
+ }
+
+ if (coordDistance > maxSnapDistance) continue
+ if (!a.isInternalSegment || !b.isInternalSegment) continue
+
+ const [target, moving] = a.length >= b.length ? [a, b] : [b, a]
+ const movingTrace = outputTraces[moving.traceIndex]!
+ const candidatePath = simplifyPath(
+ getPathWithSegmentCoord(
+ movingTrace.tracePath,
+ moving.segmentIndex,
+ moving.orientation,
+ target.coord,
+ ),
+ )
+
+ if (pathsEqual(candidatePath, movingTrace.tracePath)) continue
+ if (
+ !isSafeTracePath(
+ candidatePath,
+ movingTrace.globalConnNetId,
+ outputTraces,
+ moving.traceIndex,
+ )
+ ) {
+ continue
+ }
+
+ outputTraces[moving.traceIndex] = {
+ ...movingTrace,
+ tracePath: candidatePath,
+ }
+ coalescedSegmentCount++
+ changedThisPass = true
+ break
+ }
+ if (changedThisPass) break
+ }
+
+ if (!changedThisPass) break
+ }
+
+ return {
+ traces: outputTraces,
+ coalescedSegmentCount,
+ }
+}
diff --git a/tests/examples/__snapshots__/example02.snap.svg b/tests/examples/__snapshots__/example02.snap.svg
index 3815fdc0..c705c977 100644
--- a/tests/examples/__snapshots__/example02.snap.svg
+++ b/tests/examples/__snapshots__/example02.snap.svg
@@ -53,20 +53,16 @@ x+" data-x="1" data-y="-0.1" cx="500.20151295522464" cy="341.11637364700744" r="
x+" data-x="1" data-y="0.1" cx="500.20151295522464" cy="324.189420823755" r="3" fill="hsl(323, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
@@ -156,22 +152,22 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53
-
+
-
+
-
+
-
+
@@ -196,23 +192,19 @@ orientation: y+" data-x="1.4571549750000001" data-y="0.29999999999999966" cx="53
+globalConnNetId: connectivity_net0" data-x="-1.4574283249999997" data-y="1.5274186000000005" x="283.75416992460123" y="184.33736239143013" width="16.926952823252577" height="38.08564385231816" fill="#ef444466" stroke="#ef4444" stroke-width="0.011815475714285715" />
+globalConnNetId: connectivity_net1" data-x="-3.0434765500000003" data-y="-0.4250000000000004" x="149.51935252470935" y="349.5798500586337" width="16.926952823252492" height="38.085643852318185" 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" />
diff --git a/tests/examples/__snapshots__/example13.snap.svg b/tests/examples/__snapshots__/example13.snap.svg
index 3fe23640..cdecd241 100644
--- a/tests/examples/__snapshots__/example13.snap.svg
+++ b/tests/examples/__snapshots__/example13.snap.svg
@@ -77,36 +77,28 @@ x-" data-x="3.435" data-y="3" cx="513.5461404526988" cy="157.49274521183986" r="
x+" data-x="4.5649999999999995" data-y="3" cx="586.9994196169471" cy="157.49274521183986" r="3" fill="hsl(58, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -193,7 +185,7 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492
-
+
@@ -202,13 +194,13 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492
-
+
-
+
-
+
@@ -251,43 +243,35 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492
+globalConnNetId: connectivity_net3" data-x="3.75" data-y="-1.725" x="527.5217643644805" y="450.0058038305282" 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" />
diff --git a/tests/examples/__snapshots__/example14.snap.svg b/tests/examples/__snapshots__/example14.snap.svg
index b91ea230..ea2ffb80 100644
--- a/tests/examples/__snapshots__/example14.snap.svg
+++ b/tests/examples/__snapshots__/example14.snap.svg
@@ -73,36 +73,28 @@ y-" data-x="1.2000000000000002" data-y="1.1500000000000001" cx="419.076923076923
y+" data-x="1.2000000000000002" data-y="2.25" cx="419.07692307692315" cy="98.15384615384613" r="3" fill="hsl(349, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -147,13 +139,13 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44
-
+
-
+
@@ -168,7 +160,7 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44
-
+
@@ -196,43 +188,35 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44
+globalConnNetId: connectivity_net4" data-x="1.3510000000000002" data-y="-0.5760000000000001" x="423.4707692307693" y="322.24" width="17.230769230769226" height="38.769230769230774" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net4" data-x="-1.2000000000000002" data-y="-2.6750000000000003" x="203.6923076923077" y="503.0769230769231" width="17.230769230769255" height="38.76923076923083" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net1" data-x="-1.6250000000000002" data-y="-0.30000000000000004" x="156.30769230769232" y="309.2307692307692" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net3" data-x="1.6250000000000002" data-y="0.09999999999999998" x="436.3076923076924" y="274.7692307692308" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net5" data-x="-1.3010000000000002" data-y="0.7260000000000001" x="194.99076923076925" y="210.0676923076923" width="17.230769230769255" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net5" data-x="3.2" data-y="0.525" x="582.7692307692308" y="227.3846153846154" width="17.230769230769283" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net0" data-x="-1.5500000000000003" data-y="0.32500000000000007" x="173.53846153846155" y="244.6153846153846" width="17.230769230769226" height="38.769230769230745" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" />
+globalConnNetId: connectivity_net2" data-x="1.4755000000000003" data-y="-1.0694553500000001" x="434.19692307692316" y="364.7530763076923" width="17.230769230769226" height="38.769230769230774" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" />
diff --git a/tests/examples/__snapshots__/example15.snap.svg b/tests/examples/__snapshots__/example15.snap.svg
index 5d1b2f9d..8dd372fe 100644
--- a/tests/examples/__snapshots__/example15.snap.svg
+++ b/tests/examples/__snapshots__/example15.snap.svg
@@ -317,40 +317,31 @@ y+" data-x="-2.025" data-y="-1.6000000000000003" cx="318.5204755614267" cy="471.
y-" data-x="-2.025" data-y="-2.7" cx="318.5204755614267" cy="526.0237780713342" r="3" fill="hsl(111, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -776,13 +767,13 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354
-
+
-
+
-
+
@@ -791,22 +782,22 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354
-
+
-
+
-
+
-
+
-
+
@@ -821,22 +812,22 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354
-
+
-
+
-
+
-
+
@@ -882,48 +873,39 @@ orientation: y-" data-x="-2.49" data-y="-2.9000000000000004" cx="295.58784676354
+globalConnNetId: connectivity_net0" data-x="-1.3099999999999998" data-y="1.4250000000000016" x="348.85072655217965" y="311.4927344782034" width="9.863496257155475" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net0" data-x="0.29250000000000076" data-y="6.9300000000000015" x="427.8819903126376" y="40.00000000000006" width="9.863496257155475" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net6" data-x="-1.3099999999999998" data-y="3.0250000000000017" x="348.85072655217965" y="232.58476442095986" width="9.863496257155475" height="22.192866578599762" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net6" data-x="-3.7550000000000003" data-y="4.193333333333335" x="228.26948480845445" y="174.96550711874357" width="9.863496257155447" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net9" data-x="-2.25" data-y="-1.4000000000000004" x="296.327608982827" y="456.97930427124606" width="22.192866578599705" height="9.863496257155475" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net8" data-x="-2.25" data-y="2.200000000000001" x="296.327608982827" y="279.4363716424482" width="22.192866578599705" height="9.863496257155475" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net10" data-x="0.29250000000000076" data-y="4.980000000000002" x="427.8819903126376" y="136.16908850726549" width="9.863496257155475" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net10" data-x="-3.7550000000000003" data-y="2.2433333333333345" x="228.26948480845445" y="271.13459562600906" width="9.863496257155447" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" />
+globalConnNetId: connectivity_net10" data-x="-2.49" data-y="-3.1250000000000004" x="290.6560986349626" y="535.8872743284896" width="9.863496257155418" height="22.192866578599705" fill="#00000066" stroke="#000000" stroke-width="0.020276785714285723" />
diff --git a/tests/examples/__snapshots__/example18.snap.svg b/tests/examples/__snapshots__/example18.snap.svg
index 8c7f05fc..f7fbea9d 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" />
diff --git a/tests/examples/__snapshots__/example19.snap.svg b/tests/examples/__snapshots__/example19.snap.svg
index ac5ba82d..42c624fc 100644
--- a/tests/examples/__snapshots__/example19.snap.svg
+++ b/tests/examples/__snapshots__/example19.snap.svg
@@ -49,20 +49,16 @@ y+" data-x="2.0034928" data-y="-0.8350319000000007" cx="300.34928" cy="422.74112
x-" data-x="1.5541992" data-y="-1.2014628704999997" cx="255.41992000000002" cy="459.38421955" r="3" fill="hsl(247, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
@@ -95,13 +91,13 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438
-
+
-
+
diff --git a/tests/examples/__snapshots__/example21.snap.svg b/tests/examples/__snapshots__/example21.snap.svg
index 85810dff..aa4f4573 100644
--- a/tests/examples/__snapshots__/example21.snap.svg
+++ b/tests/examples/__snapshots__/example21.snap.svg
@@ -57,36 +57,28 @@ y+" data-x="2.9752723250000006" data-y="0.6000000000000003" cx="345.123488706365
y-" data-x="2.9752723250000006" data-y="-0.4999999999999998" cx="345.1234887063655" cy="307.25530458590003" r="3" fill="hsl(83, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -167,7 +159,7 @@ orientation: x-" data-x="2.9752723250000006" data-y="-0.5999999999999999" cx="34
-
+
@@ -201,43 +193,35 @@ orientation: x-" data-x="2.9752723250000006" data-y="-0.5999999999999999" cx="34
+globalConnNetId: connectivity_net3" data-x="-1.551" data-y="-0.801" x="49.58247775496234" y="316.9016655258955" width="12.776637006616482" height="19.16495550992471" fill="#00000066" stroke="#000000" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net3" data-x="1.551" data-y="-0.801" x="247.7481177275838" y="316.9016655258955" width="12.776637006616511" height="19.16495550992471" fill="#00000066" stroke="#000000" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net3" data-x="2.8699999999999997" data-y="-2.65" x="332.0100387862194" y="435.0216746520648" width="12.776637006616511" height="19.164955509924653" fill="#00000066" stroke="#000000" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net1" data-x="-1.6004999999999998" data-y="0.75" x="46.42026009582477" y="217.81884553958474" width="12.776637006616482" height="19.16495550992471" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net1" data-x="2.9752723250000006" data-y="0.9500000000000005" x="338.7351702030573" y="205.04220853296823" width="12.776637006616454" height="19.16495550992471" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net0" data-x="6.2425" data-y="7.771561172376096e-16" x="547.4560803102897" y="262.53707506274236" width="12.776637006616397" height="25.553274013232908" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net0" data-x="1.501" data-y="1.2009999999999998" x="244.5539584759297" y="185.8133698380105" width="12.776637006616482" height="25.553274013232937" fill="#ef444466" stroke="#ef4444" stroke-width="0.01565357142857143" />
+globalConnNetId: connectivity_net2" data-x="2.675272325000001" data-y="-0.5999999999999999" x="306.79357768651613" y="307.25530458590003" width="38.32991101984936" height="12.776637006616511" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01565357142857143" />
diff --git a/tests/examples/__snapshots__/example29.snap.svg b/tests/examples/__snapshots__/example29.snap.svg
index 7f0d4649..f566b1dd 100644
--- a/tests/examples/__snapshots__/example29.snap.svg
+++ b/tests/examples/__snapshots__/example29.snap.svg
@@ -489,188 +489,142 @@ x+" data-x="-8.4" data-y="-16.6" cx="198.31710258539454" cy="392.52251162760695"
x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r="3" fill="hsl(226, 100%, 50%, 0.8)" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -844,7 +798,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -853,16 +807,16 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
-
+
-
+
@@ -871,7 +825,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -880,7 +834,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -889,7 +843,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -898,7 +852,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -907,7 +861,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -916,7 +870,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
@@ -925,19 +879,19 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
-
+
-
+
@@ -946,13 +900,13 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36
-
+
-
+
@@ -1086,7 +1040,7 @@ globalConnNetId: connectivity_net5" data-x="-6.262500000000001" data-y="-3.775"
+globalConnNetId: connectivity_net6" data-x="-4" data-y="-5.775" x="275.26364065639507" y="194.7878033288731" width="3.578908747488356" height="8.052544681848872" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+globalConnNetId: connectivity_net10" data-x="-4" data-y="-9.775" x="275.26364065639507" y="266.365978278641" width="3.578908747488356" height="8.052544681848872" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+globalConnNetId: connectivity_net12" data-x="-4" data-y="-12.225" x="275.26364065639507" y="310.2076104353738" width="3.578908747488356" height="8.052544681848929" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+globalConnNetId: connectivity_net14" data-x="-4" data-y="-13.775" x="275.26364065639507" y="337.94415322840894" width="3.578908747488356" height="8.052544681848872" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+globalConnNetId: connectivity_net18" data-x="-4" data-y="-17.775" x="275.26364065639507" y="409.52232817817674" width="3.578908747488356" height="8.052544681848985" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+globalConnNetId: connectivity_net22" data-x="-4" data-y="-21.775" x="275.26364065639507" y="481.10050312794465" width="3.578908747488356" height="8.052544681848985" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+globalConnNetId: connectivity_net26" data-x="-4" data-y="-25.775" x="275.26364065639507" y="552.6786780777126" width="3.578908747488356" height="8.052544681848872" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />
+ ({
+ mspPairId,
+ globalConnNetId,
+ tracePath,
+ mspConnectionPairIds: [mspPairId],
+ pinIds: [],
+ pins: [],
+ }) as unknown as SolvedTracePath
+
+test("coalesces close overlapping internal same-net segments", () => {
+ const lower = makeTrace("lower", "net1", [
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 4, y: 1 },
+ { x: 4, y: 0 },
+ ])
+ const upper = makeTrace("upper", "net1", [
+ { x: 0, y: 2 },
+ { x: 0, y: 1.12 },
+ { x: 4, y: 1.12 },
+ { x: 4, y: 2 },
+ ])
+
+ const result = coalesceSameNetTraces([lower, upper])
+
+ expect(result.coalescedSegmentCount).toBe(1)
+ expect(result.traces[1]!.tracePath).toEqual([
+ { x: 0, y: 2 },
+ { x: 0, y: 1 },
+ { x: 4, y: 1 },
+ { x: 4, y: 2 },
+ ])
+})
+
+test("trims redundant endpoint overlap already covered by same-net trace", () => {
+ const shortBranch = makeTrace("short", "net1", [
+ { x: 0, y: 0 },
+ { x: 0, y: 2 },
+ { x: 1, y: 2 },
+ ])
+ const trunk = makeTrace("trunk", "net1", [
+ { x: 0, y: 0 },
+ { x: 0, y: 5 },
+ { x: 1, y: 5 },
+ ])
+
+ const result = coalesceSameNetTraces([shortBranch, trunk])
+
+ expect(result.coalescedSegmentCount).toBe(1)
+ expect(result.traces[0]!.tracePath).toEqual([
+ { x: 0, y: 2 },
+ { x: 1, y: 2 },
+ ])
+})
+
+test("does not coalesce close overlapping segments from different nets", () => {
+ const lower = makeTrace("lower", "net1", [
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 4, y: 1 },
+ { x: 4, y: 0 },
+ ])
+ const upper = makeTrace("upper", "net2", [
+ { x: 0, y: 2 },
+ { x: 0, y: 1.12 },
+ { x: 4, y: 1.12 },
+ { x: 4, y: 2 },
+ ])
+
+ const result = coalesceSameNetTraces([lower, upper])
+
+ expect(result.coalescedSegmentCount).toBe(0)
+ expect(result.traces[1]!.tracePath).toEqual(upper.tracePath)
+})
+
+test("rejects same-net coalescing that would cross a different net", () => {
+ const lower = makeTrace("lower", "net1", [
+ { x: 0, y: 0 },
+ { x: 0, y: 1 },
+ { x: 4, y: 1 },
+ { x: 4, y: 0 },
+ ])
+ const upper = makeTrace("upper", "net1", [
+ { x: 0, y: 2 },
+ { x: 0, y: 1.12 },
+ { x: 4, y: 1.12 },
+ { x: 4, y: 2 },
+ ])
+ const blocker = makeTrace("blocker", "net2", [
+ { x: 2, y: 0.9 },
+ { x: 2, y: 1.05 },
+ ])
+
+ const result = coalesceSameNetTraces([lower, upper, blocker])
+
+ expect(result.coalescedSegmentCount).toBe(0)
+ expect(result.traces[1]!.tracePath).toEqual(upper.tracePath)
+})