diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index d9b3a32..312e537 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -4,12 +4,13 @@ import { type PipelineStep, } from "@tscircuit/solver-utils" import type { SimpleRouteJson } from "./types/srj-types" -import type { GridFill3DOptions } from "./rectdiff-types" +import type { GridFill3DOptions, XYRect } from "./rectdiff-types" import type { CapacityMeshNode } from "./types/capacity-mesh-types" import type { GraphicsObject } from "graphics-debug" import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline" import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline" import { createBaseVisualization } from "./rectdiff-visualization" +import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInverseRects" export interface RectDiffPipelineInput { simpleRouteJson: SimpleRouteJson @@ -19,6 +20,8 @@ export interface RectDiffPipelineInput { export class RectDiffPipeline extends BasePipelineSolver { rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline gapFillSolver?: GapFillSolverPipeline + // Represents cutout area + boardCutoutArea: XYRect[] = [] override pipelineDef: PipelineStep[] = [ definePipelineStep( @@ -28,6 +31,7 @@ export class RectDiffPipeline extends BasePipelineSolver { simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson, gridOptions: rectDiffPipeline.inputProblem.gridOptions, + boardCutoutArea: rectDiffPipeline.boardCutoutArea, }, ], ), @@ -39,11 +43,29 @@ export class RectDiffPipeline extends BasePipelineSolver meshNodes: rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput() .meshNodes ?? [], + simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson, + boardCutoutArea: rectDiffPipeline.boardCutoutArea, }, ], ), ] + override _setup(): void { + const srj = this.inputProblem.simpleRouteJson + const bounds: XYRect = { + x: srj.bounds.minX, + y: srj.bounds.minY, + width: srj.bounds.maxX - srj.bounds.minX, + height: srj.bounds.maxY - srj.bounds.minY, + } + + if (srj.outline && srj.outline.length > 2) { + this.boardCutoutArea = computeInverseRects(bounds, srj.outline) + } else { + this.boardCutoutArea = [] + } + } + override getConstructorParams() { return [this.inputProblem] } diff --git a/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts b/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts index 109cbe5..79535fd 100644 --- a/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +++ b/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts @@ -7,14 +7,29 @@ import { EDGE_MAP, EDGES } from "./edge-constants" import { getBoundsFromCorners } from "./getBoundsFromCorners" import type { Bounds } from "@tscircuit/math-utils" import { midpoint, segmentToBoxMinDistance } from "@tscircuit/math-utils" +import type { XYRect } from "lib/rectdiff-types" const EPS = 1e-4 +type ExpandEdgesToEmptySpaceSolverInput = { + inputMeshNodes: CapacityMeshNode[] + segmentsWithAdjacentEmptySpace: Array + boardCutoutArea?: XYRect[] +} + export interface ExpandedSegment { segment: SegmentWithAdjacentEmptySpace newNode: CapacityMeshNode } +type IndexedCapacityMeshNode = CapacityMeshNode & { + minX: number + minY: number + maxX: number + maxY: number + _boardCutout?: boolean +} + export class ExpandEdgesToEmptySpaceSolver extends BaseSolver { unprocessedSegments: Array = [] expandedSegments: Array = [] @@ -26,26 +41,49 @@ export class ExpandEdgesToEmptySpaceSolver extends BaseSolver { lastSearchCorner2: { x: number; y: number } | null = null lastExpandedSegment: ExpandedSegment | null = null - rectSpatialIndex: RBush + rectSpatialIndex: RBush - constructor( - private input: { - inputMeshNodes: CapacityMeshNode[] - segmentsWithAdjacentEmptySpace: Array - }, - ) { + constructor(private input: ExpandEdgesToEmptySpaceSolverInput) { super() this.unprocessedSegments = [...this.input.segmentsWithAdjacentEmptySpace] - this.rectSpatialIndex = new RBush() - this.rectSpatialIndex.load( + const allLayerZs = Array.from( + new Set( + this.input.inputMeshNodes.flatMap((n) => n.availableZ ?? []).sort(), + ), + ) + + // Add board cutout areas as special mesh nodes that block expansion + const boardCutoutNodes: IndexedCapacityMeshNode[] = ( + this.input.boardCutoutArea ?? [] + ).map((rect, idx) => { + const center = { + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + } + return { + capacityMeshNodeId: `board-void-${idx}`, + center, + width: rect.width, + height: rect.height, + availableZ: allLayerZs.length ? allLayerZs : [0], + layer: "board-void", + _boardCutout: true, + minX: center.x - rect.width / 2, + minY: center.y - rect.height / 2, + maxX: center.x + rect.width / 2, + maxY: center.y + rect.height / 2, + } + }) + const meshNodesForIndex: IndexedCapacityMeshNode[] = this.input.inputMeshNodes.map((n) => ({ ...n, minX: n.center.x - n.width / 2, minY: n.center.y - n.height / 2, maxX: n.center.x + n.width / 2, maxY: n.center.y + n.height / 2, - })), - ) + })) + this.rectSpatialIndex = new RBush() + this.rectSpatialIndex.load([...meshNodesForIndex, ...boardCutoutNodes]) } override _step() { @@ -105,7 +143,9 @@ export class ExpandEdgesToEmptySpaceSolver extends BaseSolver { this.lastSearchBounds = searchBounds collidingNodes = this.rectSpatialIndex .search(searchBounds) - .filter((n) => n.availableZ.includes(segment.z)) + .filter( + (n) => (n._boardCutout ?? false) || n.availableZ.includes(segment.z), + ) .filter( (n) => n.capacityMeshNodeId !== segment.parent.capacityMeshNodeId, ) diff --git a/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts b/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts index 465117a..0b3b12d 100644 --- a/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +++ b/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts @@ -6,12 +6,17 @@ import { import type { SimpleRouteJson } from "lib/types/srj-types" import type { CapacityMeshNode } from "lib/types/capacity-mesh-types" import type { GraphicsObject } from "graphics-debug" +import type { XYRect } from "lib/rectdiff-types" import { FindSegmentsWithAdjacentEmptySpaceSolver } from "./FindSegmentsWithAdjacentEmptySpaceSolver" import { ExpandEdgesToEmptySpaceSolver } from "./ExpandEdgesToEmptySpaceSolver" -export class GapFillSolverPipeline extends BasePipelineSolver<{ +type GapFillSolverPipelineInput = { meshNodes: CapacityMeshNode[] -}> { + simpleRouteJson?: SimpleRouteJson + boardCutoutArea?: XYRect[] +} + +export class GapFillSolverPipeline extends BasePipelineSolver { findSegmentsWithAdjacentEmptySpaceSolver?: FindSegmentsWithAdjacentEmptySpaceSolver expandEdgesToEmptySpaceSolver?: ExpandEdgesToEmptySpaceSolver @@ -24,11 +29,6 @@ export class GapFillSolverPipeline extends BasePipelineSolver<{ meshNodes: gapFillPipeline.inputProblem.meshNodes, }, ], - { - onSolved: () => { - // Gap fill solver completed - }, - }, ), definePipelineStep( "expandEdgesToEmptySpaceSolver", @@ -39,13 +39,9 @@ export class GapFillSolverPipeline extends BasePipelineSolver<{ segmentsWithAdjacentEmptySpace: gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver!.getOutput() .segmentsWithAdjacentEmptySpace, + boardCutoutArea: gapFillPipeline.inputProblem.boardCutoutArea, }, ], - { - onSolved: () => { - // Gap fill solver completed - }, - }, ), ] as const diff --git a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts index 876298a..077fb21 100644 --- a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +++ b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts @@ -9,18 +9,18 @@ import { rectsToMeshNodes } from "./rectsToMeshNodes" import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types" import type { SimpleRouteJson } from "../../types/srj-types" +type RectDiffExpansionOptions = { + gridSizes: number[] + // the engine only uses gridSizes here, other options are ignored +} & Record + export type RectDiffExpansionSolverSnapshot = { srj: SimpleRouteJson layerNames: string[] layerCount: number bounds: XYRect - options: { - gridSizes: number[] - // the engine only uses gridSizes here, other options are ignored - [key: string]: any - } + options: RectDiffExpansionOptions obstaclesByLayer: XYRect[][] - boardVoidRects: XYRect[] gridIndex: number candidates: Candidate3D[] placed: Placed3D[] @@ -33,6 +33,7 @@ export type RectDiffExpansionSolverSnapshot = { export type RectDiffExpansionSolverInput = { initialSnapshot: RectDiffExpansionSolverSnapshot + boardCutoutArea: XYRect[] } /** @@ -47,13 +48,9 @@ export class RectDiffExpansionSolver extends BaseSolver { private layerNames!: string[] private layerCount!: number private bounds!: XYRect - private options!: { - gridSizes: number[] - // the engine only uses gridSizes here, other options are ignored - [key: string]: any - } + private options!: RectDiffExpansionOptions private obstaclesByLayer!: XYRect[][] - private boardVoidRects!: XYRect[] + private boardCutoutArea!: XYRect[] private gridIndex!: number private candidates!: Candidate3D[] private placed!: Placed3D[] @@ -69,6 +66,7 @@ export class RectDiffExpansionSolver extends BaseSolver { super() // Copy engine snapshot fields directly onto this solver instance Object.assign(this, this.input.initialSnapshot) + this.boardCutoutArea = this.input.boardCutoutArea } override _setup() { @@ -153,7 +151,7 @@ export class RectDiffExpansionSolver extends BaseSolver { const rects = finalizeRects({ placed: this.placed, obstaclesByLayer: this.obstaclesByLayer, - boardVoidRects: this.boardVoidRects, + boardCutoutArea: this.boardCutoutArea, }) this._meshNodes = rectsToMeshNodes(rects) this.solved = true diff --git a/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts b/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts index b10e6f0..7311889 100644 --- a/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +++ b/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts @@ -4,7 +4,7 @@ import { type PipelineStep, } from "@tscircuit/solver-utils" import type { SimpleRouteJson } from "../../types/srj-types" -import type { GridFill3DOptions } from "../../rectdiff-types" +import type { GridFill3DOptions, XYRect, Placed3D } from "../../rectdiff-types" import type { CapacityMeshNode } from "../../types/capacity-mesh-types" import { RectDiffSeedingSolver } from "../RectDiffSeedingSolver/RectDiffSeedingSolver" import { RectDiffExpansionSolver } from "../RectDiffExpansionSolver/RectDiffExpansionSolver" @@ -13,6 +13,7 @@ import type { GraphicsObject } from "graphics-debug" export type RectDiffGridSolverPipelineInput = { simpleRouteJson: SimpleRouteJson gridOptions?: Partial + boardCutoutArea?: XYRect[] } export class RectDiffGridSolverPipeline extends BasePipelineSolver { @@ -27,6 +28,7 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver [ { initialSnapshot: pipeline.rectDiffSeedingSolver!.getOutput(), + boardCutoutArea: pipeline.inputProblem.boardCutoutArea ?? [], }, ], ), @@ -52,7 +55,7 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver ({ + (placement: Placed3D, idx: number) => ({ capacityMeshNodeId: `grid-${idx}`, center: { x: placement.rect.x + placement.rect.width / 2, diff --git a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts index 5255876..b1dd340 100644 --- a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +++ b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts @@ -1,13 +1,12 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { SimpleRouteJson } from "../../types/srj-types" -import type { GraphicsObject } from "graphics-debug" +import type { GraphicsObject, Point } from "graphics-debug" import type { GridFill3DOptions, Candidate3D, Placed3D, XYRect, } from "../../rectdiff-types" -import { computeInverseRects } from "./computeInverseRects" import { buildZIndexMap, obstacleToXYRect, obstacleZs } from "./layers" import { overlaps } from "../../utils/rectdiff-geometry" import { expandRectFromSeed } from "../../utils/expandRectFromSeed" @@ -18,10 +17,10 @@ import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ" import { allLayerNode } from "../../utils/buildHardPlacedByLayer" import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint" import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps" - export type RectDiffSeedingSolverInput = { simpleRouteJson: SimpleRouteJson gridOptions?: Partial + boardCutoutArea: XYRect[] } /** @@ -44,7 +43,7 @@ export class RectDiffSeedingSolver extends BaseSolver { maxMultiLayerSpan: number | undefined } private obstaclesByLayer!: XYRect[][] - private boardVoidRects!: XYRect[] + private boardCutoutArea!: XYRect[] private gridIndex!: number private candidates!: Candidate3D[] private placed!: Placed3D[] @@ -77,20 +76,18 @@ export class RectDiffSeedingSolver extends BaseSolver { () => [], ) - let boardVoidRects: XYRect[] = [] - if (srj.outline && srj.outline.length > 2) { - boardVoidRects = computeInverseRects(bounds, srj.outline as any) - for (const voidR of boardVoidRects) { - for (let z = 0; z < layerCount; z++) { - obstaclesByLayer[z]!.push(voidR) - } + this.boardCutoutArea = this.input.boardCutoutArea + + for (const voidR of this.boardCutoutArea) { + for (let z = 0; z < layerCount; z++) { + obstaclesByLayer[z]!.push(voidR) } } for (const obstacle of srj.obstacles ?? []) { - const rect = obstacleToXYRect(obstacle as any) + const rect = obstacleToXYRect(obstacle) if (!rect) continue - const zLayers = obstacleZs(obstacle as any, zIndexByName) + const zLayers = obstacleZs(obstacle, zIndexByName) const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount) if (invalidZs.length) { throw new Error( @@ -148,7 +145,6 @@ export class RectDiffSeedingSolver extends BaseSolver { this.bounds = bounds this.options = options this.obstaclesByLayer = obstaclesByLayer - this.boardVoidRects = boardVoidRects this.gridIndex = 0 this.candidates = [] this.placed = [] @@ -353,7 +349,6 @@ export class RectDiffSeedingSolver extends BaseSolver { bounds: this.bounds, options: this.options, obstaclesByLayer: this.obstaclesByLayer, - boardVoidRects: this.boardVoidRects, gridIndex: this.gridIndex, candidates: this.candidates, placed: this.placed, @@ -436,7 +431,7 @@ export class RectDiffSeedingSolver extends BaseSolver { } // board void rects (early visualization of mask) - if (this.boardVoidRects) { + if (this.boardCutoutArea.length > 0) { let outlineBBox: { x: number y: number @@ -457,7 +452,7 @@ export class RectDiffSeedingSolver extends BaseSolver { } } - for (const r of this.boardVoidRects) { + for (const r of this.boardCutoutArea) { if (outlineBBox && !overlaps(r, outlineBBox)) { continue } @@ -476,13 +471,13 @@ export class RectDiffSeedingSolver extends BaseSolver { // candidate positions (where expansion will later start from) if (this.candidates?.length) { for (const cand of this.candidates) { - points.push({ + const candidatePoint: NonNullable[number] = { x: cand.x, y: cand.y, - fill: "#9333ea", - stroke: "#6b21a8", + color: "#9333ea", label: `z:${cand.z}`, - } as any) + } + points.push(candidatePoint) } } diff --git a/lib/utils/finalizeRects.ts b/lib/utils/finalizeRects.ts index 37f20db..c97f3f9 100644 --- a/lib/utils/finalizeRects.ts +++ b/lib/utils/finalizeRects.ts @@ -3,7 +3,7 @@ import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types" export function finalizeRects(params: { placed: Placed3D[] obstaclesByLayer: XYRect[][] - boardVoidRects: XYRect[] + boardCutoutArea: XYRect[] }): Rect3d[] { // Convert all placed (free space) nodes to output format const out: Rect3d[] = params.placed.map((p) => ({ @@ -31,7 +31,7 @@ export function finalizeRects(params: { }) // Append obstacle nodes to the output - const voidSet = new Set(params.boardVoidRects || []) + const voidSet = new Set(params.boardCutoutArea || []) for (const [rect, layerIndices] of layersByObstacleRect.entries()) { if (voidSet.has(rect)) continue // Skip void rects diff --git a/tests/__snapshots__/board-outline.snap.svg b/tests/__snapshots__/board-outline.snap.svg index 0e96bff..2af9aeb 100644 --- a/tests/__snapshots__/board-outline.snap.svg +++ b/tests/__snapshots__/board-outline.snap.svg @@ -1,19 +1,23 @@ -