diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 5d10b18..73ebab0 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -58,6 +58,8 @@ export class RectDiffPipeline extends BasePipelineSolver zIndexByName: rectDiffPipeline.zIndexByName, minTraceWidth: rectDiffPipeline.inputProblem.simpleRouteJson.minTraceWidth, + minViaDiameter: + rectDiffPipeline.inputProblem.simpleRouteJson.minViaDiameter, obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance, }, ], diff --git a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts index b37a364..6a2f0a4 100644 --- a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +++ b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts @@ -8,6 +8,7 @@ import { expandRectFromSeed } from "../../utils/expandRectFromSeed" import { finalizeRects } from "../../utils/finalizeRects" import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps" import { rectsToMeshNodes } from "./rectsToMeshNodes" +import { refineFreeLayerOverlaps } from "./refineFreeLayerOverlaps" import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types" import type { Obstacle } from "../../types/srj-types" import RBush from "rbush" @@ -15,6 +16,8 @@ import { rectToTree } from "../../utils/rectToTree" import { sameTreeRect } from "../../utils/sameTreeRect" export type RectDiffExpansionSolverInput = { + minTraceWidth?: number + minViaDiameter?: number layerNames: string[] layerCount: number bounds: XYRect @@ -143,7 +146,20 @@ export class RectDiffExpansionSolver extends BaseSolver { boardVoidRects: this.input.boardVoidRects, obstacleClearance: this.input.obstacleClearance, }) - this._meshNodes = rectsToMeshNodes(rects) + let refinedRects = rects + if (this.input.minTraceWidth !== undefined) { + refinedRects = refineFreeLayerOverlaps({ + rects, + simpleRouteJson: { + obstacles: this.input.obstacles, + minTraceWidth: this.input.minTraceWidth, + minViaDiameter: this.input.minViaDiameter, + }, + zIndexByName: this.input.zIndexByName, + obstacleClearance: this.input.obstacleClearance, + }) + } + this._meshNodes = rectsToMeshNodes(refinedRects) this.solved = true } diff --git a/lib/solvers/RectDiffExpansionSolver/refineFreeLayerOverlaps.ts b/lib/solvers/RectDiffExpansionSolver/refineFreeLayerOverlaps.ts new file mode 100644 index 0000000..6f0f75b --- /dev/null +++ b/lib/solvers/RectDiffExpansionSolver/refineFreeLayerOverlaps.ts @@ -0,0 +1,183 @@ +import { boundsIntersection } from "@tscircuit/math-utils" +import type { Obstacle, SimpleRouteJson } from "../../types/srj-types" +import type { Rect3d, XYRect } from "../../rectdiff-types" +import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers" +import { EPS, overlaps, subtractRect2D } from "../../utils/rectdiff-geometry" +import { padRect } from "../../utils/padRect" + +type PreparedObstacle = { obstacle: Obstacle; rect: XYRect; layers: number[] } + +const rectToXYRect = (rect: Rect3d): XYRect => ({ + x: rect.minX, + y: rect.minY, + width: rect.maxX - rect.minX, + height: rect.maxY - rect.minY, +}) + +const rectBounds = (rect: XYRect) => ({ + minX: rect.x, + minY: rect.y, + maxX: rect.x + rect.width, + maxY: rect.y + rect.height, +}) + +function createRefinedRect({ + source, + rect, +}: { + source: Rect3d + rect: XYRect +}): Rect3d { + return { ...source, ...rectBounds(rect) } +} + +function isCopperPourTransit( + { + rect, + zLayers, + skippedLayers, + }: { + rect: XYRect + zLayers: number[] + skippedLayers: number[] + }, + obstacles: PreparedObstacle[], +): boolean { + const crossingObstacles = obstacles.filter( + (entry) => + entry.layers.some((z) => z >= zLayers[0]! && z <= zLayers.at(-1)!) && + overlaps(entry.rect, rect), + ) + if (crossingObstacles.some((entry) => !entry.obstacle.isCopperPour)) + return false + return skippedLayers.every((z) => { + let uncovered = [rect] + for (const entry of crossingObstacles) { + if (!entry.layers.includes(z)) continue + uncovered = uncovered.flatMap((piece) => + subtractRect2D(piece, entry.rect), + ) + } + return uncovered.length === 0 + }) +} + +/** Refine free rectangles across copper-pour layers while retaining each remainder. */ +export function refineFreeLayerOverlaps({ + rects, + simpleRouteJson, + zIndexByName, + obstacleClearance = 0, +}: { + rects: Rect3d[] + simpleRouteJson: Pick< + SimpleRouteJson, + "obstacles" | "minTraceWidth" | "minViaDiameter" + > + zIndexByName: Map + obstacleClearance?: number +}): Rect3d[] { + const freeRects = rects.filter((region) => !region.isObstacle) + const singleLayerRects = freeRects.filter( + (region) => region.zLayers.length === 1, + ) + const multilayerRects = freeRects.filter( + (region) => region.zLayers.length > 1, + ) + if (singleLayerRects.length === 0 || multilayerRects.length === 0) { + return rects + } + 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( + rects.map((region) => { + const original = rectToXYRect(region) + return [region, { original, remaining: [original] }] + }), + ) + const sharedRects: Rect3d[] = [] + const minViaSize = Math.max( + simpleRouteJson.minViaDiameter ?? 0, + simpleRouteJson.minTraceWidth, + ) + + for (const single of singleLayerRects) { + for (const multi of multilayerRects) { + if (multi.zLayers.includes(single.zLayers[0]!)) continue + const zLayers = [...single.zLayers, ...multi.zLayers].sort( + (a, b) => a - b, + ) + const skippedLayers: number[] = [] + for (let z = zLayers[0]!; z <= zLayers.at(-1)!; z++) { + if (!zLayers.includes(z)) skippedLayers.push(z) + } + if (skippedLayers.length === 0) continue + const singlePieces = pieces.get(single)!.remaining + const multiPieces = pieces.get(multi)!.remaining + let singlePieceIndex = 0 + while (singlePieceIndex < singlePieces.length) { + const singleRect = singlePieces[singlePieceIndex]! + let merged = false + for ( + let multiPieceIndex = 0; + multiPieceIndex < multiPieces.length; + multiPieceIndex++ + ) { + const multiRect = multiPieces[multiPieceIndex]! + 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 + if (!isCopperPourTransit({ rect, zLayers, skippedLayers }, obstacles)) + continue + sharedRects.push({ + ...createRefinedRect({ source: single, rect }), + zLayers, + }) + singlePieces.splice( + singlePieceIndex, + 1, + ...subtractRect2D(singleRect, rect), + ) + multiPieces.splice( + multiPieceIndex, + 1, + ...subtractRect2D(multiRect, rect), + ) + merged = true + break + } + // A split replaces this piece; inspect its remainders before advancing. + if (!merged) singlePieceIndex++ + } + } + } + if (sharedRects.length === 0) return rects + return rects + .flatMap((region) => { + const { original, remaining } = pieces.get(region)! + if (remaining.length === 1 && remaining[0] === original) return [region] + return remaining.map((rect) => + createRefinedRect({ source: region, rect }), + ) + }) + .concat(sharedRects) +} diff --git a/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts b/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts index c0e1420..66957af 100644 --- a/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +++ b/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts @@ -27,6 +27,7 @@ export type RectDiffGridSolverPipelineInput = { outline?: Pick layerCount: number minTraceWidth: number + minViaDiameter?: number obstacleClearance?: number gridOptions?: Partial boardVoidRects?: XYRect[] @@ -95,6 +96,8 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver { + const rects: Rect3d[] = [ + { + minX: 0, + minY: 0, + maxX: 2, + maxY: 2, + zLayers: [0], + }, + { + minX: 1, + minY: 0, + maxX: 3, + maxY: 2, + zLayers: [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 = { + rects, + 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.zLayers.length === 3)).toMatchObject({ + minX: 1, + minY: 0, + maxX: 2, + maxY: 2, + zLayers: [0, 2, 3], + }) + expect(refined.find((node) => node.zLayers.length === 1)).toMatchObject({ + minX: 0, + minY: 0, + maxX: 1, + maxY: 2, + zLayers: [0], + }) + expect(refined.find((node) => node.zLayers.length === 2)).toMatchObject({ + minX: 2, + minY: 0, + maxX: 3, + maxY: 2, + zLayers: [2, 3], + }) + const solid = structuredClone(input) + solid.simpleRouteJson.obstacles[0]!.isCopperPour = false + expect(refineFreeLayerOverlaps(solid)).toEqual(rects) + const uncovered = structuredClone(input) + uncovered.simpleRouteJson.obstacles[0]!.width = 0.5 + expect(refineFreeLayerOverlaps(uncovered)).toEqual(rects) + const narrow = structuredClone(input) + narrow.rects[1]!.minX = 1.9 + expect(refineFreeLayerOverlaps(narrow)).toEqual(narrow.rects) + const touching = structuredClone(input) + touching.rects[1]!.minX = 2 + expect(refineFreeLayerOverlaps(touching)).toEqual(touching.rects) + const target = structuredClone(input) + target.rects[0]!.isObstacle = true + expect(refineFreeLayerOverlaps(target)).toEqual(target.rects) +}) 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..700b10e 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 @@ -454,906 +454,2180 @@ z:0,1" data-x="-11.4" data-y="-16" x="264.90291262135923" y="713.1165048543689" z:0,1" data-x="21.712500000000002" data-y="-17.075" x="792.1310679611652" y="730.2330097087379" width="7.165048543689295" height="7.165048543689295" fill="#fee2e2" stroke="#ef4444" stroke-width="0.06280487804878049"/>Shared top/inner2 free regions: 0