diff --git a/lib/utils/canonicalizeLayeredRects.ts b/lib/utils/canonicalizeLayeredRects.ts new file mode 100644 index 0000000..94a3940 --- /dev/null +++ b/lib/utils/canonicalizeLayeredRects.ts @@ -0,0 +1,205 @@ +import type { Placed3D, Rect3d } from "../rectdiff-types" +import { EPS, containsPoint } from "./rectdiff-geometry" + +type RectRun = { + minX: number + maxX: number + minY: number + maxY: number + zLayers: number[] +} + +const sortNumbers = (a: number, b: number) => a - b + +function dedupeSortedEdges(edges: number[]): number[] { + const sorted = edges.slice().sort(sortNumbers) + const out: number[] = [] + + for (const edge of sorted) { + const last = out[out.length - 1] + if (last === undefined || Math.abs(edge - last) > EPS) { + out.push(edge) + } + } + + return out +} + +function splitIntoContiguousLayerRuns(zLayers: number[]): number[][] { + if (zLayers.length === 0) return [] + + const sorted = Array.from(new Set(zLayers)).sort(sortNumbers) + const out: number[][] = [] + let current = [sorted[0]!] + + for (let i = 1; i < sorted.length; i++) { + const z = sorted[i]! + const prev = current[current.length - 1]! + + if (z === prev + 1) { + current.push(z) + continue + } + + out.push(current) + current = [z] + } + + out.push(current) + return out +} + +function getCellLayerRuns(params: { + minX: number + maxX: number + minY: number + maxY: number + placed: Placed3D[] +}): number[][] { + const { minX, maxX, minY, maxY, placed } = params + const midX = (minX + maxX) / 2 + const midY = (minY + maxY) / 2 + const zLayers = new Set() + + for (const placement of placed) { + if (!containsPoint(placement.rect, { x: midX, y: midY })) continue + + for (const z of placement.zLayers) { + zLayers.add(z) + } + } + + return splitIntoContiguousLayerRuns(Array.from(zLayers)) +} + +function buildRowRuns(placed: Placed3D[]): RectRun[] { + const xEdges = dedupeSortedEdges( + placed.flatMap((placement) => [ + placement.rect.x, + placement.rect.x + placement.rect.width, + ]), + ) + const yEdges = dedupeSortedEdges( + placed.flatMap((placement) => [ + placement.rect.y, + placement.rect.y + placement.rect.height, + ]), + ) + + const runs: RectRun[] = [] + + for (let yi = 0; yi < yEdges.length - 1; yi++) { + const minY = yEdges[yi]! + const maxY = yEdges[yi + 1]! + if (maxY - minY <= EPS) continue + + const openRuns = new Map() + + for (let xi = 0; xi < xEdges.length - 1; xi++) { + const minX = xEdges[xi]! + const maxX = xEdges[xi + 1]! + if (maxX - minX <= EPS) continue + + const layerRuns = getCellLayerRuns({ + minX, + maxX, + minY, + maxY, + placed, + }) + + const seenKeys = new Set() + + for (const zLayers of layerRuns) { + const key = zLayers.join(",") + seenKeys.add(key) + const existing = openRuns.get(key) + + if (existing && Math.abs(existing.maxX - minX) <= EPS) { + existing.maxX = maxX + continue + } + + if (existing) runs.push(existing) + + openRuns.set(key, { + minX, + maxX, + minY, + maxY, + zLayers, + }) + } + + for (const [key, run] of openRuns) { + if (seenKeys.has(key)) continue + runs.push(run) + openRuns.delete(key) + } + } + + for (const run of openRuns.values()) { + runs.push(run) + } + } + + return runs +} + +function mergeRunsVertically(runs: RectRun[]): Rect3d[] { + const sorted = runs.slice().sort((a, b) => { + if (Math.abs(a.minY - b.minY) > EPS) return a.minY - b.minY + if (Math.abs(a.maxY - b.maxY) > EPS) return a.maxY - b.maxY + if (Math.abs(a.minX - b.minX) > EPS) return a.minX - b.minX + if (Math.abs(a.maxX - b.maxX) > EPS) return a.maxX - b.maxX + return a.zLayers.join(",").localeCompare(b.zLayers.join(",")) + }) + + const out: Rect3d[] = [] + const active = new Map() + + for (const run of sorted) { + const key = [ + run.minX.toFixed(9), + run.maxX.toFixed(9), + run.zLayers.join(","), + ].join(":") + const existing = active.get(key) + + if (existing && Math.abs(existing.maxY - run.minY) <= EPS) { + existing.maxY = run.maxY + continue + } + + if (existing) out.push(existing) + + active.set(key, { + minX: run.minX, + minY: run.minY, + maxX: run.maxX, + maxY: run.maxY, + zLayers: run.zLayers.slice(), + }) + } + + for (const rect of active.values()) { + out.push(rect) + } + + return out + .filter((rect) => rect.maxX - rect.minX > EPS && rect.maxY - rect.minY > EPS) + .sort((a, b) => { + const aKey = a.zLayers.join(",") + const bKey = b.zLayers.join(",") + if (aKey !== bKey) return aKey.localeCompare(bKey) + if (Math.abs(a.minY - b.minY) > EPS) return a.minY - b.minY + if (Math.abs(a.minX - b.minX) > EPS) return a.minX - b.minX + if (Math.abs(a.maxY - b.maxY) > EPS) return a.maxY - b.maxY + return a.maxX - b.maxX + }) +} + +export function canonicalizeLayeredRects(placed: Placed3D[]): Rect3d[] { + if (placed.length === 0) return [] + return mergeRunsVertically(buildRowRuns(placed)) +} diff --git a/lib/utils/finalizeRects.ts b/lib/utils/finalizeRects.ts index 2cf12a5..a842904 100644 --- a/lib/utils/finalizeRects.ts +++ b/lib/utils/finalizeRects.ts @@ -4,6 +4,8 @@ import { obstacleToXYRect, obstacleZs, } from "../solvers/RectDiffSeedingSolver/layers" +import { canonicalizeLayeredRects } from "./canonicalizeLayeredRects" +import { intersectRect2D, subtractRect2D, EPS } from "./rectdiff-geometry" export function finalizeRects(params: { placed: Placed3D[] @@ -12,8 +14,119 @@ export function finalizeRects(params: { zIndexByName: Map obstacleClearance?: number }): Rect3d[] { - // Convert all placed (free space) nodes to output format - const out: Rect3d[] = params.placed.map((p) => ({ + const promotedRects = canonicalizeLayeredRects(params.placed).filter( + (rect) => rect.zLayers.length > 1, + ) + + let freePlacements: Placed3D[] = params.placed.map((placement) => ({ + rect: { ...placement.rect }, + zLayers: placement.zLayers.slice().sort((a, b) => a - b), + })) + + for (const promoted of promotedRects) { + const promotedRect: XYRect = { + x: promoted.minX, + y: promoted.minY, + width: promoted.maxX - promoted.minX, + height: promoted.maxY - promoted.minY, + } + + const nextPlacements: Placed3D[] = [] + + for (const placement of freePlacements) { + const sharedZ = placement.zLayers.filter((z) => promoted.zLayers.includes(z)) + if (sharedZ.length === 0) { + nextPlacements.push(placement) + continue + } + + const overlapCore = intersectRect2D(placement.rect, promotedRect) + if (!overlapCore) { + nextPlacements.push(placement) + continue + } + + const outsideParts = subtractRect2D(placement.rect, promotedRect) + for (const part of outsideParts) { + if (part.width > EPS && part.height > EPS) { + nextPlacements.push({ + rect: part, + zLayers: placement.zLayers.slice(), + }) + } + } + + const unaffectedZ = placement.zLayers.filter( + (z) => !promoted.zLayers.includes(z), + ) + if ( + unaffectedZ.length > 0 && + overlapCore.width > EPS && + overlapCore.height > EPS + ) { + nextPlacements.push({ + rect: overlapCore, + zLayers: unaffectedZ, + }) + } + } + + nextPlacements.push({ + rect: promotedRect, + zLayers: promoted.zLayers.slice(), + }) + + const deduped = new Map() + for (const placement of nextPlacements) { + if (placement.rect.width <= EPS || placement.rect.height <= EPS) continue + placement.zLayers = Array.from(new Set(placement.zLayers)).sort((a, b) => a - b) + if (placement.zLayers.length === 0) continue + + const key = [ + placement.rect.x.toFixed(9), + placement.rect.y.toFixed(9), + placement.rect.width.toFixed(9), + placement.rect.height.toFixed(9), + ].join(":") + const existing = deduped.get(key) + if (!existing) { + deduped.set(key, placement) + continue + } + for (const z of placement.zLayers) { + if (!existing.zLayers.includes(z)) existing.zLayers.push(z) + } + existing.zLayers.sort((a, b) => a - b) + } + + freePlacements = Array.from(deduped.values()) + } + + if (process.env.RECTDIFF_DEBUG_FINALIZE === "1") { + const countByKey = (items: Array<{ zLayers: number[] }>) => { + const counts = new Map() + for (const item of items) { + const key = item.zLayers.join(",") + counts.set(key, (counts.get(key) ?? 0) + 1) + } + return Object.fromEntries(counts) + } + + console.log( + "[finalizeRects] promoted shared regions", + JSON.stringify( + { + inputCombos: countByKey(params.placed), + promotedCombos: countByKey(promotedRects), + outputCombos: countByKey(freePlacements), + }, + null, + 2, + ), + ) + } + + const out: Rect3d[] = freePlacements.map((p) => ({ minX: p.rect.x, minY: p.rect.y, maxX: p.rect.x + p.rect.width, diff --git a/lib/utils/rectdiff-geometry.ts b/lib/utils/rectdiff-geometry.ts index f2deaec..08c76bf 100644 --- a/lib/utils/rectdiff-geometry.ts +++ b/lib/utils/rectdiff-geometry.ts @@ -85,3 +85,20 @@ export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] { return out.filter((r) => r.width > EPS && r.height > EPS) } + +export function intersectRect2D(A: XYRect, B: XYRect): XYRect | null { + const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width]) + const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height]) + if (!Xi || !Yi) return null + + const [minX, maxX] = Xi + const [minY, maxY] = Yi + if (maxX - minX <= EPS || maxY - minY <= EPS) return null + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + } +} diff --git a/lib/utils/resizeSoftOverlaps.ts b/lib/utils/resizeSoftOverlaps.ts index ce52ad5..6b701ee 100644 --- a/lib/utils/resizeSoftOverlaps.ts +++ b/lib/utils/resizeSoftOverlaps.ts @@ -1,6 +1,11 @@ import type { RTreeRect } from "lib/types/capacity-mesh-types" import type { Placed3D } from "../rectdiff-types" -import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry" +import { + overlaps, + subtractRect2D, + intersectRect2D, + EPS, +} from "./rectdiff-geometry" import type RBush from "rbush" import { rectToTree } from "./rectToTree" @@ -30,18 +35,40 @@ export function resizeSoftOverlaps( if (sharedZ.length === 0) continue if (!overlaps(old.rect, newR)) continue - // Carve the overlap on the shared layers + // Carve only the shared layers. Non-overlapped pieces keep the full + // original layer set, and the overlapped core keeps only unaffected layers. const parts = subtractRect2D(old.rect, newR) + const overlapCore = intersectRect2D(old.rect, newR) + const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z)) + + if ( + process.env.RECTDIFF_DEBUG_OVERLAPS === "1" && + old.zLayers.length > 1 && + newZs.length === 1 + ) { + console.log( + "[resizeSoftOverlaps] carving multi-layer node", + JSON.stringify( + { + oldRect: old.rect, + oldZs: old.zLayers, + newcomerRect: newR, + newcomerZs: newZs, + sharedZ, + unaffectedZ, + partsCount: parts.length, + hasOverlapCore: Boolean(overlapCore), + }, + null, + 2, + ), + ) + } // We will replace `old` entirely; re-add unaffected layers (same rect object). removeIdx.push(i) - const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z)) - if (unaffectedZ.length > 0) { - toAdd.push({ rect: old.rect, zLayers: unaffectedZ }) - } - - // Re-add carved pieces for affected layers, dropping tiny slivers + // Outside the overlap, the old node still exists on all of its layers. const minW = Math.min( params.options.minSingle.width, params.options.minMulti.width, @@ -52,9 +79,86 @@ export function resizeSoftOverlaps( ) for (const p of parts) { if (p.width + EPS >= minW && p.height + EPS >= minH) { - toAdd.push({ rect: p, zLayers: sharedZ.slice() }) + toAdd.push({ rect: p, zLayers: old.zLayers.slice() }) } } + + // Inside the overlap, only the old node's non-shared layers remain. + if ( + overlapCore && + unaffectedZ.length > 0 && + overlapCore.width + EPS >= minW && + overlapCore.height + EPS >= minH + ) { + toAdd.push({ rect: overlapCore, zLayers: unaffectedZ }) + } + + // If the overlap core is too small to keep as a standalone unaffected node, + // the newcomer fully consumes that shared region for practical purposes. + if ( + process.env.RECTDIFF_DEBUG_OVERLAPS === "1" && + overlapCore && + unaffectedZ.length > 0 && + (overlapCore.width + EPS < minW || overlapCore.height + EPS < minH) + ) { + console.log( + "[resizeSoftOverlaps] dropped tiny overlap core", + JSON.stringify( + { + overlapCore, + unaffectedZ, + minW, + minH, + }, + null, + 2, + ), + ) + } + } + + for (const p of toAdd) { + p.zLayers = Array.from(new Set(p.zLayers)).sort((a, b) => a - b) + if (p.zLayers.length === 0) { + throw new Error("resizeSoftOverlaps produced an empty zLayers placement") + } + } + + const mergedToAdd: typeof params.placed = [] + const seenReplacements = new Map() + for (const p of toAdd) { + const key = [ + p.rect.x.toFixed(9), + p.rect.y.toFixed(9), + p.rect.width.toFixed(9), + p.rect.height.toFixed(9), + ].join(":") + const existing = seenReplacements.get(key) + if (!existing) { + seenReplacements.set(key, { + rect: p.rect, + zLayers: p.zLayers.slice(), + }) + continue + } + + for (const z of p.zLayers) { + if (!existing.zLayers.includes(z)) existing.zLayers.push(z) + } + existing.zLayers.sort((a, b) => a - b) + } + + for (const merged of seenReplacements.values()) { + if ( + process.env.RECTDIFF_DEBUG_OVERLAPS === "1" && + merged.zLayers.length > 1 + ) { + console.log( + "[resizeSoftOverlaps] merged replacement", + JSON.stringify(merged, null, 2), + ) + } + mergedToAdd.push(merged) } // Remove fully overlapped nodes and keep indexes in sync @@ -81,7 +185,7 @@ export function resizeSoftOverlaps( }) // Add replacements - for (const p of toAdd) { + for (const p of mergedToAdd) { params.placed.push(p) for (const z of p.zLayers) { if (params.placedIndexByLayer) { diff --git a/tests/__snapshots__/board-outline.snap.svg b/tests/__snapshots__/board-outline.snap.svg index 8f9b27f..80d6fb1 100644 --- a/tests/__snapshots__/board-outline.snap.svg +++ b/tests/__snapshots__/board-outline.snap.svg @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts b/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts new file mode 100644 index 0000000..73793ed --- /dev/null +++ b/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "bun:test" +import inputProblems from "./node-solver-input.json" +import { + getBounds, + getSvgFromGraphicsObject, + mergeGraphics, + stackGraphicsVertically, + type GraphicsObject, + type Rect, +} from "graphics-debug" +import { RectDiffPipeline } from "lib/RectDiffPipeline" +import { makeCapacityMeshNodeWithLayerInfo } from "tests/fixtures/makeCapacityMeshNodeWithLayerInfo" +import { makeSimpleRouteOutlineGraphics } from "tests/fixtures/makeSimpleRouteOutlineGraphics" + +test("node-solver input repro snapshot", async () => { + const problem = inputProblems[0]! + const solver = new RectDiffPipeline(problem) + const outline = makeSimpleRouteOutlineGraphics(problem.simpleRouteJson) + + solver.solve() + + const { meshNodes } = solver.getOutput() + const freeNodes = meshNodes.filter((node) => !node._containsObstacle) + const merged01FreeNodes = freeNodes.filter( + (node) => node.availableZ.join(",") === "0,1", + ) + + expect(merged01FreeNodes.length).toBeGreaterThan(10) + + const rectsByCombo = makeCapacityMeshNodeWithLayerInfo(meshNodes) + const allGraphicsObjects: GraphicsObject[] = [] + + for (let z = 0; z < problem.simpleRouteJson.layerCount; z++) { + const layerRects: Rect[] = [] + + for (const [key, rects] of rectsByCombo) { + const layers = key + .split(",") + .map((value) => Number.parseInt(value, 10)) + .filter((value) => !Number.isNaN(value)) + + if (layers.includes(z)) { + layerRects.push(...rects) + } + } + + let labelY = 0 + + if (layerRects.length > 0) { + let maxY = -Infinity + + for (const rect of layerRects) { + const top = rect.center.y + rect.height * (2 / 3) + + if (top > maxY) maxY = top + } + + labelY = maxY + } + + const graphics: GraphicsObject = { + title: `RectDiffPipeline - z${z}`, + texts: [ + { + anchorSide: "top_right", + text: `Layer z=${z}`, + x: 0, + y: labelY, + fontSize: 0.5, + }, + ], + coordinateSystem: "cartesian", + rects: layerRects, + points: [], + lines: [], + } + + allGraphicsObjects.push(mergeGraphics(graphics, outline)) + } + + const stackedGraphics = stackGraphicsVertically(allGraphicsObjects) + const bounds = getBounds(stackedGraphics) + const boundsWidth = Math.max(1, bounds.maxX - bounds.minX) + const boundsHeight = Math.max(1, bounds.maxY - bounds.minY) + const svgWidth = 640 + const svgHeight = Math.max( + svgWidth, + Math.ceil((boundsHeight / boundsWidth) * svgWidth), + ) + + const svg = getSvgFromGraphicsObject(stackedGraphics, { + svgWidth, + svgHeight, + }) + + await expect(svg).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/solver/repros/node-solver-input/node-solver-input.json b/tests/solver/repros/node-solver-input/node-solver-input.json new file mode 100644 index 0000000..7c07f68 --- /dev/null +++ b/tests/solver/repros/node-solver-input/node-solver-input.json @@ -0,0 +1 @@ +[{"simpleRouteJson":{"bounds":{"maxX":10,"maxY":5,"minX":-10,"minY":-5},"obstacles":[{"type":"rect","width":1,"center":{"x":-2.15,"y":2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_0","connectivity_net8","source_trace_3","source_port_10","source_port_0","pcb_smtpad_0","pcb_port_0","pcb_smtpad_10","pcb_port_10"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_1","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":0},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_2","connectivity_net24","source_trace_9","source_port_16","source_trace_1","source_port_2","source_net_1","pcb_smtpad_2","pcb_port_2","pcb_smtpad_16","pcb_port_16"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":-1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_3","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":-2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_4","connectivity_net74","source_port_4","pcb_smtpad_4","pcb_port_4"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":-2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_5","connectivity_net13","source_trace_5","source_port_12","source_port_5","pcb_smtpad_5","pcb_port_5","pcb_smtpad_12","pcb_port_12"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":-1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_6","connectivity_net21","source_trace_8","source_port_15","source_port_6","pcb_smtpad_6","pcb_port_6","pcb_smtpad_15","pcb_port_15"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":0},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_7","connectivity_net18","source_trace_7","source_port_14","source_port_7","pcb_smtpad_7","pcb_port_7","pcb_smtpad_14","pcb_port_14"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_8","connectivity_net31","source_trace_12","source_port_19","source_port_8","pcb_smtpad_8","pcb_port_8","pcb_smtpad_19","pcb_port_19"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_9","connectivity_net28","source_trace_11","source_port_18","source_port_9","pcb_smtpad_9","pcb_port_9","pcb_smtpad_18","pcb_port_18"],"zLayers":[0]},{"type":"rect","width":0.54,"center":{"x":-2.1500000000000004,"y":4.16},"height":0.64,"layers":["top"],"connectedTo":["pcb_smtpad_10","connectivity_net8","source_trace_3","source_port_10","source_port_0","pcb_smtpad_0","pcb_port_0","pcb_smtpad_10","pcb_port_10"],"zLayers":[0]},{"type":"rect","width":0.54,"center":{"x":-1.1300000000000006,"y":4.16},"height":0.64,"layers":["top"],"connectedTo":["pcb_smtpad_11","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":0.64,"center":{"x":3.97,"y":-3.2861261293105146},"height":0.54,"layers":["top"],"connectedTo":["pcb_smtpad_12","connectivity_net13","source_trace_5","source_port_12","source_port_5","pcb_smtpad_5","pcb_port_5","pcb_smtpad_12","pcb_port_12"],"zLayers":[0]},{"type":"rect","width":0.64,"center":{"x":3.97,"y":-4.306126129310514},"height":0.54,"layers":["top"],"connectedTo":["pcb_smtpad_13","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":4.15,"y":0.8238738706894861},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_14","connectivity_net18","source_trace_7","source_port_14","source_port_7","pcb_smtpad_7","pcb_port_7","pcb_smtpad_14","pcb_port_14"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":4.15,"y":-0.4461261293105139},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_15","connectivity_net21","source_trace_8","source_port_15","source_port_6","pcb_smtpad_6","pcb_port_6","pcb_smtpad_15","pcb_port_15"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":4.15,"y":-1.716126129310514},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_16","connectivity_net24","source_trace_9","source_port_16","source_trace_1","source_port_2","source_net_1","pcb_smtpad_2","pcb_port_2","pcb_smtpad_16","pcb_port_16"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":8.45,"y":-1.716126129310514},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_17","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":8.45,"y":-0.4461261293105139},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_18","connectivity_net28","source_trace_11","source_port_18","source_port_9","pcb_smtpad_9","pcb_port_9","pcb_smtpad_18","pcb_port_18"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":8.45,"y":0.8238738706894861},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_19","connectivity_net31","source_trace_12","source_port_19","source_port_8","pcb_smtpad_8","pcb_port_8","pcb_smtpad_19","pcb_port_19"],"zLayers":[0]},{"type":"rect","width":20,"center":{"x":0,"y":0},"height":10,"layers":["bottom"],"connectedTo":["source_net_0","unnamedsubcircuit760_connectivity_net0","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"isCopperPour":true,"zLayers":[3]},{"type":"rect","width":20,"center":{"x":0,"y":0},"height":10,"layers":["inner2"],"connectedTo":["source_net_1","unnamedsubcircuit760_connectivity_net1","connectivity_net24","source_trace_9","source_port_16","source_trace_1","source_port_2","source_net_1","pcb_smtpad_2","pcb_port_2","pcb_smtpad_16","pcb_port_16"],"isCopperPour":true,"zLayers":[2]},{"obstacleId":"escape-via-obstacle:escape-via:source_trace_9:pcb_port_16:inner2:0","type":"rect","layers":["top","inner1","inner2"],"zLayers":[0,1,2],"center":{"x":3.3500000000000005,"y":-1.716126129310514},"width":0.3,"height":0.3,"connectedTo":["source_trace_9"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_1:pcb_port_2:inner2:2","type":"rect","layers":["top","inner1","inner2"],"zLayers":[0,1,2],"center":{"x":-1.3499999999999999,"y":0},"width":0.3,"height":0.3,"connectedTo":["source_net_1"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_11:bottom:3","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":-0.5600000000000005,"y":4.16},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_13:bottom:4","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":3.3500000000000005,"y":-4.306126129310514},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_1:bottom:6","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":-1.3499999999999999,"y":1.27},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_3:bottom:8","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":-1.3499999999999999,"y":-1.27},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_17:bottom:9","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":7.6499999999999995,"y":-1.716126129310514},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]}],"layerCount":4,"connections":[{"pointsToConnect":[{"x":-1.1300000000000006,"y":4.16,"layer":"top","pointId":"pcb_port_11","pcb_port_id":"pcb_port_11"},{"x":-0.5600000000000005,"y":4.16,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_11:bottom:3","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}}],"name":"source_net_0_mst0","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":3.3500000000000005,"y":-4.306126129310514,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_13:bottom:4","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}},{"x":3.97,"y":-4.306126129310514,"layer":"top","pointId":"pcb_port_13","pcb_port_id":"pcb_port_13"}],"name":"source_net_0_mst1","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":7.6499999999999995,"y":-1.716126129310514,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_17:bottom:9","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}},{"x":8.45,"y":-1.716126129310514,"layer":"top","pointId":"pcb_port_17","pcb_port_id":"pcb_port_17"}],"name":"source_net_0_mst2","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":-2.15,"y":1.27,"layer":"top","pointId":"pcb_port_1","pcb_port_id":"pcb_port_1"},{"x":-1.3499999999999999,"y":1.27,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_1:bottom:6","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}}],"name":"source_net_0_mst3","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":-2.15,"y":-1.27,"layer":"top","pointId":"pcb_port_3","pcb_port_id":"pcb_port_3"},{"x":-1.3499999999999999,"y":-1.27,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_3:bottom:8","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}}],"name":"source_net_0_mst4","rootConnectionName":"source_net_0"},{"name":"source_trace_12","pointsToConnect":[{"x":8.45,"y":0.8238738706894861,"layer":"top","pointId":"pcb_port_19","pcb_port_id":"pcb_port_19"},{"x":2.15,"y":1.27,"layer":"top","pointId":"pcb_port_8","pcb_port_id":"pcb_port_8"}],"source_trace_id":"source_trace_12","rootConnectionName":"source_trace_12"},{"name":"source_trace_11","pointsToConnect":[{"x":8.45,"y":-0.4461261293105139,"layer":"top","pointId":"pcb_port_18","pcb_port_id":"pcb_port_18"},{"x":2.15,"y":2.54,"layer":"top","pointId":"pcb_port_9","pcb_port_id":"pcb_port_9"}],"source_trace_id":"source_trace_11","rootConnectionName":"source_trace_11"},{"pointsToConnect":[{"x":3.3500000000000005,"y":-1.716126129310514,"layer":"top","pointId":"escape-via:source_trace_9:pcb_port_16:inner2:0","terminalVia":{"toLayer":"inner2","viaDiameter":0.3}},{"x":4.15,"y":-1.716126129310514,"layer":"top","pointId":"pcb_port_16","pcb_port_id":"pcb_port_16"}],"name":"source_trace_9__source_net_1_mst0","rootConnectionName":"source_trace_9__source_net_1","mergedConnectionNames":{"$ref":"$.simpleRouteJson.connections[8].mergedConnectionNames"}},{"pointsToConnect":[{"x":-2.15,"y":0,"layer":"top","pointId":"pcb_port_2","pcb_port_id":"pcb_port_2"},{"x":-1.3499999999999999,"y":0,"layer":"top","pointId":"escape-via:source_net_1:pcb_port_2:inner2:2","terminalVia":{"toLayer":"inner2","viaDiameter":0.3}}],"name":"source_trace_9__source_net_1_mst1","rootConnectionName":"source_trace_9__source_net_1","mergedConnectionNames":["source_trace_9","source_net_1"]},{"name":"source_trace_8","pointsToConnect":[{"x":4.15,"y":-0.4461261293105139,"layer":"top","pointId":"pcb_port_15","pcb_port_id":"pcb_port_15"},{"x":2.15,"y":-1.27,"layer":"top","pointId":"pcb_port_6","pcb_port_id":"pcb_port_6"}],"source_trace_id":"source_trace_8","rootConnectionName":"source_trace_8"},{"name":"source_trace_7","pointsToConnect":[{"x":4.15,"y":0.8238738706894861,"layer":"top","pointId":"pcb_port_14","pcb_port_id":"pcb_port_14"},{"x":2.15,"y":0,"layer":"top","pointId":"pcb_port_7","pcb_port_id":"pcb_port_7"}],"source_trace_id":"source_trace_7","rootConnectionName":"source_trace_7"},{"name":"source_trace_5","pointsToConnect":[{"x":3.97,"y":-3.2861261293105146,"layer":"top","pointId":"pcb_port_12","pcb_port_id":"pcb_port_12"},{"x":2.15,"y":-2.54,"layer":"top","pointId":"pcb_port_5","pcb_port_id":"pcb_port_5"}],"source_trace_id":"source_trace_5","rootConnectionName":"source_trace_5"},{"name":"source_trace_3","pointsToConnect":[{"x":-2.1500000000000004,"y":4.16,"layer":"top","pointId":"pcb_port_10","pcb_port_id":"pcb_port_10"},{"x":-2.15,"y":2.54,"layer":"top","pointId":"pcb_port_0","pcb_port_id":"pcb_port_0"}],"source_trace_id":"source_trace_3","rootConnectionName":"source_trace_3"}],"minTraceWidth":0.15}}] \ No newline at end of file diff --git a/tests/solver/transitivity/__snapshots__/transitivity.snap.svg b/tests/solver/transitivity/__snapshots__/transitivity.snap.svg index 560398e..4db7018 100644 --- a/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +++ b/tests/solver/transitivity/__snapshots__/transitivity.snap.svg @@ -1,4 +1,4 @@ -Layer z=0Layer z=1