From da30a92155233ea953de116b98e1b692a1d2af50 Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Fri, 11 Sep 2026 23:03:17 +0530 Subject: [PATCH 1/3] up --- .../OuterLayerContainmentMergeSolver.ts | 6 +- .../refineFreeLayerOverlaps.ts | 167 + tests/free-layer-overlap-transit.test.ts | 88 + .../rv1106-free-layer-overlap.snap.svg | 4302 +++++++++++++---- .../rv1106-free-layer-overlap.test.ts | 2 +- 5 files changed, 3701 insertions(+), 864 deletions(-) create mode 100644 lib/solvers/OuterLayerContainmentMergeSolver/refineFreeLayerOverlaps.ts create mode 100644 tests/free-layer-overlap-transit.test.ts diff --git a/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts b/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts index b471918..a0839ef 100644 --- a/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +++ b/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts @@ -7,6 +7,7 @@ import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers" import { getColorForZLayer } from "../../utils/getColorForZLayer" import { subtractRect2D, overlaps, EPS } from "../../utils/rectdiff-geometry" import { padRect } from "../../utils/padRect" +import { refineFreeLayerOverlaps } from "./refineFreeLayerOverlaps" type OuterLayerContainmentMergeSolverInput = { meshNodes: CapacityMeshNode[] @@ -100,7 +101,10 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver { } override _step() { - this.outputNodes = this.processOuterLayerContainmentMerges() + this.outputNodes = refineFreeLayerOverlaps({ + ...this.input, + meshNodes: this.processOuterLayerContainmentMerges(), + }) this.solved = true } diff --git a/lib/solvers/OuterLayerContainmentMergeSolver/refineFreeLayerOverlaps.ts b/lib/solvers/OuterLayerContainmentMergeSolver/refineFreeLayerOverlaps.ts new file mode 100644 index 0000000..14b0e3a --- /dev/null +++ b/lib/solvers/OuterLayerContainmentMergeSolver/refineFreeLayerOverlaps.ts @@ -0,0 +1,167 @@ +import { boundsIntersection } from "@tscircuit/math-utils" +import type { CapacityMeshNode } from "../../types/capacity-mesh-types" +import type { SimpleRouteJson } from "../../types/srj-types" +import type { XYRect } from "../../rectdiff-types" +import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers" +import { EPS, overlaps, subtractRect2D } from "../../utils/rectdiff-geometry" +import { padRect } from "../../utils/padRect" + +const nodeRect = (node: CapacityMeshNode): XYRect => ({ + x: node.center.x - node.width / 2, + y: node.center.y - node.height / 2, + width: node.width, + height: node.height, +}) + +const rectBounds = (rect: XYRect) => ({ + minX: rect.x, + minY: rect.y, + maxX: rect.x + rect.width, + maxY: rect.y + rect.height, +}) + +function createRefinedNode( + { source, rect }: { source: CapacityMeshNode; rect: XYRect }, + ids: { used: Set; next: number }, +): CapacityMeshNode { + let capacityMeshNodeId = `free_overlap_${ids.next++}` + while (ids.used.has(capacityMeshNodeId)) { + capacityMeshNodeId = `free_overlap_${ids.next++}` + } + ids.used.add(capacityMeshNodeId) + return { + ...source, + capacityMeshNodeId, + center: { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }, + width: rect.width, + height: rect.height, + } +} + +/** Refine free meshes across copper-pour layers while retaining each remainder. */ +export function refineFreeLayerOverlaps({ + meshNodes, + simpleRouteJson, + zIndexByName, + obstacleClearance = 0, +}: { + meshNodes: CapacityMeshNode[] + simpleRouteJson: SimpleRouteJson + zIndexByName: Map + obstacleClearance?: number +}): CapacityMeshNode[] { + const freeNodes = meshNodes.filter( + (node) => + !node._containsObstacle && !node._containsTarget && !node._strawNode, + ) + const singleLayerNodes = freeNodes.filter( + (node) => node.availableZ.length === 1, + ) + const multilayerNodes = freeNodes.filter((node) => node.availableZ.length > 1) + if (singleLayerNodes.length === 0 || multilayerNodes.length === 0) { + return meshNodes + } + const obstacles = simpleRouteJson.obstacles.flatMap((obstacle) => { + const rect = obstacleToXYRect(obstacle) + if (!rect) return [] + return [ + { + obstacle, + rect: padRect(rect, obstacleClearance), + layers: obstacleZs(obstacle, zIndexByName), + }, + ] + }) + const pieces = new Map(meshNodes.map((node) => [node, [nodeRect(node)]])) + const sharedNodes: CapacityMeshNode[] = [] + const minViaSize = Math.max( + simpleRouteJson.minViaDiameter ?? 0, + simpleRouteJson.minTraceWidth, + ) + const ids = { + used: new Set(meshNodes.map((node) => node.capacityMeshNodeId)), + next: 0, + } + + for (const single of singleLayerNodes) { + for (const multi of multilayerNodes) { + if (multi.availableZ.includes(single.availableZ[0]!)) continue + const availableZ = [...single.availableZ, ...multi.availableZ].sort( + (a, b) => a - b, + ) + const skippedLayers: number[] = [] + for (let z = availableZ[0]!; z <= availableZ.at(-1)!; z++) { + if (!availableZ.includes(z)) skippedLayers.push(z) + } + if (skippedLayers.length === 0) continue + const singlePieces = pieces.get(single)! + const multiPieces = pieces.get(multi)! + for (let a = 0; a < singlePieces.length; a++) { + for (let b = 0; b < multiPieces.length; b++) { + const singleRect = singlePieces[a]! + const multiRect = multiPieces[b]! + const overlap = boundsIntersection( + rectBounds(singleRect), + rectBounds(multiRect), + ) + if (!overlap) continue + const rect = { + x: overlap.minX, + y: overlap.minY, + width: overlap.maxX - overlap.minX, + height: overlap.maxY - overlap.minY, + } + if (rect.width + EPS < minViaSize || rect.height + EPS < minViaSize) + continue + const crossingObstacles = obstacles.filter( + (entry) => + entry.layers.some( + (z) => z >= availableZ[0]! && z <= availableZ.at(-1)!, + ) && overlaps(entry.rect, rect), + ) + if (crossingObstacles.some((entry) => !entry.obstacle.isCopperPour)) + continue + const coveredByPour = skippedLayers.every((z) => { + let uncovered = [rect] + for (const entry of crossingObstacles) { + if (entry.layers.includes(z)) + uncovered = uncovered.flatMap((piece) => + subtractRect2D(piece, entry.rect), + ) + } + return uncovered.length === 0 + }) + if (!coveredByPour) continue + sharedNodes.push({ + ...createRefinedNode({ source: single, rect }, ids), + availableZ, + layer: `z${availableZ.join(",")}`, + }) + singlePieces.splice(a, 1, ...subtractRect2D(singleRect, rect)) + multiPieces.splice(b, 1, ...subtractRect2D(multiRect, rect)) + a-- + break + } + } + } + } + if (sharedNodes.length === 0) return meshNodes + return meshNodes + .flatMap((node) => { + const remaining = pieces.get(node)! + if (remaining.length === 1) { + const original = nodeRect(node) + if ( + remaining[0]!.x === original.x && + remaining[0]!.y === original.y && + remaining[0]!.width === original.width && + remaining[0]!.height === original.height + ) + return [node] + } + return remaining.map((rect) => + createRefinedNode({ source: node, rect }, ids), + ) + }) + .concat(sharedNodes) +} diff --git a/tests/free-layer-overlap-transit.test.ts b/tests/free-layer-overlap-transit.test.ts new file mode 100644 index 0000000..5d98ecf --- /dev/null +++ b/tests/free-layer-overlap-transit.test.ts @@ -0,0 +1,88 @@ +import { expect, test } from "bun:test" +import { refineFreeLayerOverlaps } from "lib/solvers/OuterLayerContainmentMergeSolver/refineFreeLayerOverlaps" +import type { CapacityMeshNode } from "lib/types/capacity-mesh-types" +import type { SimpleRouteJson } from "lib/types/srj-types" + +test("free-layer refinement preserves remainders and rejects blocked or narrow transit", () => { + const meshNodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "top", + center: { x: 1, y: 1 }, + width: 2, + height: 2, + layer: "top", + availableZ: [0], + }, + { + capacityMeshNodeId: "lower", + center: { x: 2, y: 1 }, + width: 2, + height: 2, + layer: "bottom", + availableZ: [2, 3], + }, + ] + const simpleRouteJson: SimpleRouteJson = { + bounds: { minX: 0, maxX: 3, minY: 0, maxY: 2 }, + layerCount: 4, + minTraceWidth: 0.15, + minViaDiameter: 0.3, + connections: [], + obstacles: [ + { + type: "rect", + center: { x: 1.5, y: 1 }, + width: 3, + height: 2, + layers: ["inner1"], + connectedTo: [], + isCopperPour: true, + }, + ], + } + const input = { + meshNodes, + simpleRouteJson, + zIndexByName: new Map([ + ["top", 0], + ["inner1", 1], + ["inner2", 2], + ["bottom", 3], + ]), + } + const refined = refineFreeLayerOverlaps(input) + expect(refined).toHaveLength(3) + expect(refined.find((node) => node.availableZ.length === 3)).toMatchObject({ + center: { x: 1.5, y: 1 }, + width: 1, + height: 2, + availableZ: [0, 2, 3], + }) + expect(refined.find((node) => node.availableZ.length === 1)).toMatchObject({ + center: { x: 0.5, y: 1 }, + width: 1, + height: 2, + availableZ: [0], + }) + expect(refined.find((node) => node.availableZ.length === 2)).toMatchObject({ + center: { x: 2.5, y: 1 }, + width: 1, + height: 2, + availableZ: [2, 3], + }) + const solid = structuredClone(input) + solid.simpleRouteJson.obstacles[0]!.isCopperPour = false + expect(refineFreeLayerOverlaps(solid)).toEqual(meshNodes) + const uncovered = structuredClone(input) + uncovered.simpleRouteJson.obstacles[0]!.width = 0.5 + expect(refineFreeLayerOverlaps(uncovered)).toEqual(meshNodes) + const narrow = structuredClone(input) + narrow.meshNodes[1]!.center.x = 2.9 + expect(refineFreeLayerOverlaps(narrow)).toEqual(narrow.meshNodes) + const touching = structuredClone(input) + touching.meshNodes[1]!.center.x = 3 + expect(refineFreeLayerOverlaps(touching)).toEqual(touching.meshNodes) + const target = structuredClone(input) + target.meshNodes[0]!._containsTarget = true + expect(refineFreeLayerOverlaps(target)).toEqual(target.meshNodes) +}) diff --git a/tests/solver/rv1106-free-layer-overlap/__snapshots__/rv1106-free-layer-overlap.snap.svg b/tests/solver/rv1106-free-layer-overlap/__snapshots__/rv1106-free-layer-overlap.snap.svg index a04f8b0..1a2de1e 100644 --- a/tests/solver/rv1106-free-layer-overlap/__snapshots__/rv1106-free-layer-overlap.snap.svg +++ b/tests/solver/rv1106-free-layer-overlap/__snapshots__/rv1106-free-layer-overlap.snap.svg @@ -5684,11 +5684,476 @@ z:2,3" data-x="-1" data-y="1.9999999999999996" x="414.5728155339806" y="417.7572 z:2,3" data-x="-1" data-y="0" x="414.5728155339806" y="449.6019417475728" width="39.009708737864116" height="24.679611650485413" fill="#d1fae5" stroke="#10b981" stroke-width="0.06280487804878049"/>Shared top/inner2 free regions: 0