From 1058870f5144482d118838343c1983719eeae18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 26 Dec 2025 18:40:06 +0530 Subject: [PATCH 1/9] feat: add two-node expansion fixture demo --- lib/fixtures/twoNodeExpansionFixture.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/fixtures/twoNodeExpansionFixture.ts b/lib/fixtures/twoNodeExpansionFixture.ts index e641d8a..6aecf36 100644 --- a/lib/fixtures/twoNodeExpansionFixture.ts +++ b/lib/fixtures/twoNodeExpansionFixture.ts @@ -1,8 +1,5 @@ import RBush from "rbush" -import type { - RectDiffExpansionSolverInput, - RectDiffExpansionSolverSnapshot, -} from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" +import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" import type { SimpleRouteJson } from "../types/srj-types" import type { XYRect } from "../rectdiff-types" import type { RTreeRect } from "lib/types/capacity-mesh-types" @@ -34,7 +31,7 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { ) // Start with all-empty obstacle indexes for a "clean" scenario - const initialSnapshot: RectDiffExpansionSolverSnapshot = { + return { srj, layerNames: ["top"], layerCount, @@ -57,10 +54,6 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { edgeAnalysisDone: true, totalSeedsThisGrid: 0, consumedSeedsThisGrid: 0, - } - - return { - initialSnapshot, obstacleIndexByLayer, } } From 8c943cb6f527cc6f062278dff3086867997eb1f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 26 Dec 2025 18:43:31 +0530 Subject: [PATCH 2/9] test: cover two-node expansion fixture --- lib/fixtures/twoNodeExpansionFixture.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/fixtures/twoNodeExpansionFixture.ts b/lib/fixtures/twoNodeExpansionFixture.ts index 6aecf36..e641d8a 100644 --- a/lib/fixtures/twoNodeExpansionFixture.ts +++ b/lib/fixtures/twoNodeExpansionFixture.ts @@ -1,5 +1,8 @@ import RBush from "rbush" -import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" +import type { + RectDiffExpansionSolverInput, + RectDiffExpansionSolverSnapshot, +} from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" import type { SimpleRouteJson } from "../types/srj-types" import type { XYRect } from "../rectdiff-types" import type { RTreeRect } from "lib/types/capacity-mesh-types" @@ -31,7 +34,7 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { ) // Start with all-empty obstacle indexes for a "clean" scenario - return { + const initialSnapshot: RectDiffExpansionSolverSnapshot = { srj, layerNames: ["top"], layerCount, @@ -54,6 +57,10 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { edgeAnalysisDone: true, totalSeedsThisGrid: 0, consumedSeedsThisGrid: 0, + } + + return { + initialSnapshot, obstacleIndexByLayer, } } From 74b02f55e6583a95eb569c1014a822e72f1bbea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 26 Dec 2025 18:40:06 +0530 Subject: [PATCH 3/9] feat: add two-node expansion fixture demo --- lib/fixtures/twoNodeExpansionFixture.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/fixtures/twoNodeExpansionFixture.ts b/lib/fixtures/twoNodeExpansionFixture.ts index e641d8a..6aecf36 100644 --- a/lib/fixtures/twoNodeExpansionFixture.ts +++ b/lib/fixtures/twoNodeExpansionFixture.ts @@ -1,8 +1,5 @@ import RBush from "rbush" -import type { - RectDiffExpansionSolverInput, - RectDiffExpansionSolverSnapshot, -} from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" +import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" import type { SimpleRouteJson } from "../types/srj-types" import type { XYRect } from "../rectdiff-types" import type { RTreeRect } from "lib/types/capacity-mesh-types" @@ -34,7 +31,7 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { ) // Start with all-empty obstacle indexes for a "clean" scenario - const initialSnapshot: RectDiffExpansionSolverSnapshot = { + return { srj, layerNames: ["top"], layerCount, @@ -57,10 +54,6 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { edgeAnalysisDone: true, totalSeedsThisGrid: 0, consumedSeedsThisGrid: 0, - } - - return { - initialSnapshot, obstacleIndexByLayer, } } From d3bc33ec3e6dadb77ed4ebdfb4440f42666f6c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 26 Dec 2025 18:43:31 +0530 Subject: [PATCH 4/9] test: cover two-node expansion fixture --- lib/fixtures/twoNodeExpansionFixture.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/fixtures/twoNodeExpansionFixture.ts b/lib/fixtures/twoNodeExpansionFixture.ts index 6aecf36..e641d8a 100644 --- a/lib/fixtures/twoNodeExpansionFixture.ts +++ b/lib/fixtures/twoNodeExpansionFixture.ts @@ -1,5 +1,8 @@ import RBush from "rbush" -import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" +import type { + RectDiffExpansionSolverInput, + RectDiffExpansionSolverSnapshot, +} from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" import type { SimpleRouteJson } from "../types/srj-types" import type { XYRect } from "../rectdiff-types" import type { RTreeRect } from "lib/types/capacity-mesh-types" @@ -31,7 +34,7 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { ) // Start with all-empty obstacle indexes for a "clean" scenario - return { + const initialSnapshot: RectDiffExpansionSolverSnapshot = { srj, layerNames: ["top"], layerCount, @@ -54,6 +57,10 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { edgeAnalysisDone: true, totalSeedsThisGrid: 0, consumedSeedsThisGrid: 0, + } + + return { + initialSnapshot, obstacleIndexByLayer, } } From 44cdb7479886f01a24b00ba3ef1c886d28b5ed5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 26 Dec 2025 18:55:41 +0530 Subject: [PATCH 5/9] feat: use rbush blocker expansion --- lib/fixtures/twoNodeExpansionFixture.ts | 11 +- .../RectDiffExpansionSolver.ts | 156 ++++++------------ .../RectDiffGridSolverPipeline.ts | 29 +++- .../RectDiffSeedingSolver.ts | 11 +- lib/utils/expandRectFromSeed.ts | 95 ++++++++++- lib/utils/isSelfRect.ts | 15 ++ .../__snapshots__/should-expand-node.snap.svg | 4 +- tests/incremental-solver.test.ts | 2 +- .../rectDiffGridSolverPipeline.snap.svg | 6 +- 9 files changed, 191 insertions(+), 138 deletions(-) create mode 100644 lib/utils/isSelfRect.ts diff --git a/lib/fixtures/twoNodeExpansionFixture.ts b/lib/fixtures/twoNodeExpansionFixture.ts index e641d8a..6aecf36 100644 --- a/lib/fixtures/twoNodeExpansionFixture.ts +++ b/lib/fixtures/twoNodeExpansionFixture.ts @@ -1,8 +1,5 @@ import RBush from "rbush" -import type { - RectDiffExpansionSolverInput, - RectDiffExpansionSolverSnapshot, -} from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" +import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver" import type { SimpleRouteJson } from "../types/srj-types" import type { XYRect } from "../rectdiff-types" import type { RTreeRect } from "lib/types/capacity-mesh-types" @@ -34,7 +31,7 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { ) // Start with all-empty obstacle indexes for a "clean" scenario - const initialSnapshot: RectDiffExpansionSolverSnapshot = { + return { srj, layerNames: ["top"], layerCount, @@ -57,10 +54,6 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => { edgeAnalysisDone: true, totalSeedsThisGrid: 0, consumedSeedsThisGrid: 0, - } - - return { - initialSnapshot, obstacleIndexByLayer, } } diff --git a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts index a7108d1..61543aa 100644 --- a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +++ b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts @@ -3,28 +3,21 @@ import type { GraphicsObject } from "graphics-debug" import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types" import { expandRectFromSeed } from "../../utils/expandRectFromSeed" import { finalizeRects } from "../../utils/finalizeRects" -import { allLayerNode } from "../../utils/buildHardPlacedByLayer" import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps" import { rectsToMeshNodes } from "./rectsToMeshNodes" import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types" import type { SimpleRouteJson } from "lib/types/srj-types" -import { - buildZIndexMap, - obstacleToXYRect, - obstacleZs, -} from "../RectDiffSeedingSolver/layers" import RBush from "rbush" import { rectToTree } from "../../utils/rectToTree" import { sameTreeRect } from "../../utils/sameTreeRect" -export type RectDiffExpansionSolverSnapshot = { +export type RectDiffExpansionSolverInput = { srj: SimpleRouteJson layerNames: string[] layerCount: number bounds: XYRect options: { gridSizes: number[] - // the engine only uses gridSizes here, other options are ignored [key: string]: any } boardVoidRects: XYRect[] @@ -35,10 +28,6 @@ export type RectDiffExpansionSolverSnapshot = { edgeAnalysisDone: boolean totalSeedsThisGrid: number consumedSeedsThisGrid: number -} - -export type RectDiffExpansionSolverInput = { - initialSnapshot: RectDiffExpansionSolverSnapshot obstacleIndexByLayer: Array> } @@ -49,74 +38,25 @@ export type RectDiffExpansionSolverInput = { * and runs the EXPANSION phase, then finalizes to capacity mesh nodes. */ export class RectDiffExpansionSolver extends BaseSolver { - // Engine fields (same shape used by rectdiff/engine.ts) - private srj!: SimpleRouteJson - 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 boardVoidRects!: XYRect[] - private gridIndex!: number - private candidates!: Candidate3D[] - private placed!: Placed3D[] - private placedIndexByLayer!: Array> - private expansionIndex!: number - private edgeAnalysisDone!: boolean - private totalSeedsThisGrid!: number - private consumedSeedsThisGrid!: number - - private _meshNodes: CapacityMeshNode[] = [] - + placedIndexByLayer: Array> = [] + _meshNodes: CapacityMeshNode[] = [] constructor(private input: RectDiffExpansionSolverInput) { super() - // Copy engine snapshot fields directly onto this solver instance - Object.assign(this, this.input.initialSnapshot) } override _setup() { this.stats = { - gridIndex: this.gridIndex, - } - - if (this.input.obstacleIndexByLayer) { - } else { - const { zIndexByName } = buildZIndexMap(this.srj) - this.input.obstacleIndexByLayer = Array.from( - { length: this.layerCount }, - () => new RBush(), - ) - const insertObstacle = (rect: XYRect, z: number) => { - const tree = this.input.obstacleIndexByLayer[z] - if (tree) tree.insert(rectToTree(rect)) - } - for (const voidRect of this.boardVoidRects ?? []) { - for (let z = 0; z < this.layerCount; z++) insertObstacle(voidRect, z) - } - for (const obstacle of this.srj.obstacles ?? []) { - const rect = obstacleToXYRect(obstacle as any) - if (!rect) continue - const zLayers = - obstacle.zLayers?.length && obstacle.zLayers.length > 0 - ? obstacle.zLayers - : obstacleZs(obstacle as any, zIndexByName) - zLayers.forEach((z) => { - if (z >= 0 && z < this.layerCount) insertObstacle(rect, z) - }) - } + gridIndex: this.input.gridIndex, } this.placedIndexByLayer = Array.from( - { length: this.layerCount }, + { length: this.input.layerCount }, () => new RBush(), ) - for (const placement of this.placed ?? []) { + for (const placement of this.input.placed) { for (const z of placement.zLayers) { - const tree = this.placedIndexByLayer[z] - if (tree) tree.insert(rectToTree(placement.rect)) + const placedIndex = this.placedIndexByLayer[z] + if (placedIndex) placedIndex.insert(rectToTree(placement.rect)) } } } @@ -126,51 +66,41 @@ export class RectDiffExpansionSolver extends BaseSolver { this._stepExpansion() - this.stats.gridIndex = this.gridIndex - this.stats.placed = this.placed.length + this.stats.gridIndex = this.input.gridIndex + this.stats.placed = this.input.placed.length - if (this.expansionIndex >= this.placed.length) { + if (this.input.expansionIndex >= this.input.placed.length) { this.finalizeIfNeeded() } } private _stepExpansion(): void { - if (this.expansionIndex >= this.placed.length) { + if (this.input.expansionIndex >= this.input.placed.length) { return } - const idx = this.expansionIndex - const p = this.placed[idx]! - const lastGrid = this.options.gridSizes[this.options.gridSizes.length - 1]! - - const hardPlacedByLayer = allLayerNode({ - layerCount: this.layerCount, - placed: this.placed, - }) - - // HARD blockers only: obstacles on p.zLayers + full-stack nodes - const hardBlockers: XYRect[] = [] - for (const z of p.zLayers) { - const obstacleTree = this.input.obstacleIndexByLayer[z] - if (obstacleTree) hardBlockers.push(...obstacleTree.all()) - hardBlockers.push(...(hardPlacedByLayer[z] ?? [])) - } + const idx = this.input.expansionIndex + const p = this.input.placed[idx]! + const lastGrid = + this.input.options.gridSizes[this.input.options.gridSizes.length - 1]! const oldRect = p.rect const expanded = expandRectFromSeed({ startX: p.rect.x + p.rect.width / 2, startY: p.rect.y + p.rect.height / 2, gridSize: lastGrid, - bounds: this.bounds, - blockers: hardBlockers, + bounds: this.input.bounds, + obsticalIndexByLayer: this.input.obstacleIndexByLayer, + placedIndexByLayer: this.placedIndexByLayer, initialCellRatio: 0, maxAspectRatio: null, minReq: { width: p.rect.width, height: p.rect.height }, + zLayers: p.zLayers, }) if (expanded) { // Update placement + per-layer index (replace old rect object) - this.placed[idx] = { rect: expanded, zLayers: p.zLayers } + this.input.placed[idx] = { rect: expanded, zLayers: p.zLayers } for (const z of p.zLayers) { const tree = this.placedIndexByLayer[z] if (tree) { @@ -182,25 +112,25 @@ export class RectDiffExpansionSolver extends BaseSolver { // Carve overlapped soft neighbors (respect full-stack nodes) resizeSoftOverlaps( { - layerCount: this.layerCount, - placed: this.placed, - options: this.options, + layerCount: this.input.layerCount, + placed: this.input.placed, + options: this.input.options, placedIndexByLayer: this.placedIndexByLayer, }, idx, ) } - this.expansionIndex += 1 + this.input.expansionIndex += 1 } private finalizeIfNeeded() { if (this.solved) return const rects = finalizeRects({ - placed: this.placed, - srj: this.srj, - boardVoidRects: this.boardVoidRects, + placed: this.input.placed, + srj: this.input.srj, + boardVoidRects: this.input.boardVoidRects, }) this._meshNodes = rectsToMeshNodes(rects) this.solved = true @@ -208,25 +138,39 @@ export class RectDiffExpansionSolver extends BaseSolver { computeProgress(): number { if (this.solved) return 1 - const grids = this.options.gridSizes.length + const grids = this.input.options.gridSizes.length const base = grids / (grids + 1) - const denom = Math.max(1, this.placed.length) - const frac = denom ? this.expansionIndex / denom : 1 + const denom = Math.max(1, this.input.placed.length) + const frac = denom ? this.input.expansionIndex / denom : 1 return Math.min(0.999, base + frac * (1 / (grids + 1))) } override getOutput(): { meshNodes: CapacityMeshNode[] } { - if (!this.solved && this._meshNodes.length === 0) { - this.finalizeIfNeeded() - } - return { meshNodes: this._meshNodes } + if (this.solved) return { meshNodes: this._meshNodes } + + // Provide a live preview of the placements before finalization so debuggers + // can inspect intermediary states without forcing the solver to finish. + const previewNodes: CapacityMeshNode[] = this.input.placed.map( + (placement, idx) => ({ + capacityMeshNodeId: `expand-preview-${idx}`, + center: { + x: placement.rect.x + placement.rect.width / 2, + y: placement.rect.y + placement.rect.height / 2, + }, + width: placement.rect.width, + height: placement.rect.height, + availableZ: placement.zLayers.slice(), + layer: `z${placement.zLayers.join(",")}`, + }), + ) + return { meshNodes: previewNodes } } /** Simple visualization of expanded placements. */ override visualize(): GraphicsObject { const rects: NonNullable = [] - for (const placement of this.placed ?? []) { + for (const placement of this.input.placed ?? []) { rects.push({ center: { x: placement.rect.x + placement.rect.width / 2, diff --git a/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts b/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts index faafa3f..dcac57f 100644 --- a/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +++ b/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts @@ -48,15 +48,30 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver [ - { - initialSnapshot: { - ...pipeline.rectDiffSeedingSolver!.getOutput(), + (pipeline: RectDiffGridSolverPipeline) => { + const output = pipeline.rectDiffSeedingSolver?.getOutput() + if (!output) { + throw new Error("RectDiffSeedingSolver did not produce output") + } + return [ + { + srj: pipeline.inputProblem.simpleRouteJson, + layerNames: output.layerNames ?? [], boardVoidRects: pipeline.inputProblem.boardVoidRects ?? [], + layerCount: pipeline.inputProblem.simpleRouteJson.layerCount, + bounds: output.bounds!, + candidates: output.candidates, + consumedSeedsThisGrid: output.placed.length, + totalSeedsThisGrid: output.candidates.length, + placed: output.placed, + edgeAnalysisDone: output.edgeAnalysisDone, + gridIndex: output.gridIndex, + expansionIndex: output.expansionIndex, + obstacleIndexByLayer: pipeline.obstacleIndexByLayer, + options: output.options, }, - obstacleIndexByLayer: pipeline.obstacleIndexByLayer, - }, - ], + ] + }, ), ] diff --git a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts index 2e9811b..bfd226a 100644 --- a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +++ b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts @@ -238,23 +238,18 @@ export class RectDiffSeedingSolver extends BaseSolver { const ordered = preferMultiLayer ? attempts : attempts.reverse() for (const attempt of ordered) { - // HARD blockers only: obstacles on those layers + full-stack nodes - const hardBlockers: XYRect[] = [] - for (const z of attempt.layers) { - const obstacleLayer = this.input.obstacleIndexByLayer[z] - if (obstacleLayer) hardBlockers.push(...obstacleLayer.all()) - if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]!) - } const rect = expandRectFromSeed({ startX: cand.x, startY: cand.y, gridSize: grid, bounds: this.bounds, - blockers: hardBlockers, + obsticalIndexByLayer: this.input.obstacleIndexByLayer, + placedIndexByLayer: this.placedIndexByLayer, initialCellRatio, maxAspectRatio, minReq: attempt.minReq, + zLayers: attempt.layers, }) if (!rect) continue diff --git a/lib/utils/expandRectFromSeed.ts b/lib/utils/expandRectFromSeed.ts index d80ba49..aa904ba 100644 --- a/lib/utils/expandRectFromSeed.ts +++ b/lib/utils/expandRectFromSeed.ts @@ -1,5 +1,8 @@ +import type RBush from "rbush" import type { XYRect } from "../rectdiff-types" import { EPS, gt, gte, lt, lte, overlaps } from "./rectdiff-geometry" +import type { RTreeRect } from "lib/types/capacity-mesh-types" +import { isSelfRect } from "./isSelfRect" type ExpandDirectionParams = { r: XYRect @@ -154,17 +157,20 @@ export function expandRectFromSeed(params: { startY: number gridSize: number bounds: XYRect - blockers: XYRect[] + obsticalIndexByLayer: Array> + placedIndexByLayer: Array> initialCellRatio: number maxAspectRatio: number | null | undefined minReq: { width: number; height: number } + zLayers: number[] }): XYRect | null { const { startX, startY, gridSize, bounds, - blockers, + obsticalIndexByLayer, + placedIndexByLayer, initialCellRatio, maxAspectRatio, minReq, @@ -173,6 +179,82 @@ export function expandRectFromSeed(params: { const minSide = Math.max(1e-9, gridSize * initialCellRatio) const initialW = Math.max(minSide, minReq.width) const initialH = Math.max(minSide, minReq.height) + const blockers: XYRect[] = [] + const seen = new Set() + const toRect = (tree: RTreeRect): XYRect => ({ + x: tree.minX, + y: tree.minY, + width: tree.maxX - tree.minX, + height: tree.maxY - tree.minY, + }) + // Ignore the existing placement we are expanding so it doesn't self-block. + + const addBlocker = (rect: XYRect) => { + const key = `${rect.x}|${rect.y}|${rect.width}|${rect.height}` + if (seen.has(key)) return + seen.add(key) + blockers.push(rect) + } + const toQueryRect = (rect: XYRect) => { + const minX = Math.max(bounds.x, rect.x) + const minY = Math.max(bounds.y, rect.y) + const maxX = Math.min(bounds.x + bounds.width, rect.x + rect.width) + const maxY = Math.min(bounds.y + bounds.height, rect.y + rect.height) + if (maxX <= minX + EPS || maxY <= minY + EPS) return null + return { minX, minY, maxX, maxY } + } + const collectBlockers = (searchRect: XYRect) => { + const query = toQueryRect(searchRect) + if (!query) return + for (const z of params.zLayers) { + const blockersIndex = obsticalIndexByLayer[z] + if (blockersIndex) { + for (const entry of blockersIndex.search(query)) addBlocker(toRect(entry)) + } + + const placedLayer = placedIndexByLayer[z] + if (placedLayer) { + for (const entry of placedLayer.search(query)) { + const rect = toRect(entry) + if ( + isSelfRect({ + rect, + startX, + startY, + initialW, + initialH, + }) + ) + continue + addBlocker(rect) + } + } + } + } + const searchStripRight = (rect: XYRect): XYRect => ({ + x: rect.x, + y: rect.y, + width: bounds.x + bounds.width - rect.x, + height: rect.height, + }) + const searchStripDown = (rect: XYRect): XYRect => ({ + x: rect.x, + y: rect.y, + width: rect.width, + height: bounds.y + bounds.height - rect.y, + }) + const searchStripLeft = (rect: XYRect): XYRect => ({ + x: bounds.x, + y: rect.y, + width: rect.x - bounds.x, + height: rect.height, + }) + const searchStripUp = (rect: XYRect): XYRect => ({ + x: rect.x, + y: bounds.y, + width: rect.width, + height: rect.y - bounds.y, + }) const strategies = [ { ox: 0, oy: 0 }, @@ -192,6 +274,7 @@ export function expandRectFromSeed(params: { width: initialW, height: initialH, } + collectBlockers(r) // keep initial inside board if ( @@ -212,27 +295,35 @@ export function expandRectFromSeed(params: { improved = false const commonParams = { bounds, blockers, maxAspect: maxAspectRatio } + collectBlockers(searchStripRight(r)) const eR = maxExpandRight({ ...commonParams, r }) if (eR > 0) { r = { ...r, width: r.width + eR } + collectBlockers(r) improved = true } + collectBlockers(searchStripDown(r)) const eD = maxExpandDown({ ...commonParams, r }) if (eD > 0) { r = { ...r, height: r.height + eD } + collectBlockers(r) improved = true } + collectBlockers(searchStripLeft(r)) const eL = maxExpandLeft({ ...commonParams, r }) if (eL > 0) { r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height } + collectBlockers(r) improved = true } + collectBlockers(searchStripUp(r)) const eU = maxExpandUp({ ...commonParams, r }) if (eU > 0) { r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU } + collectBlockers(r) improved = true } } diff --git a/lib/utils/isSelfRect.ts b/lib/utils/isSelfRect.ts new file mode 100644 index 0000000..2eb841f --- /dev/null +++ b/lib/utils/isSelfRect.ts @@ -0,0 +1,15 @@ +import type { XYRect } from "lib/rectdiff-types" + +const EPS = 1e-9 + +export const isSelfRect = (params: { + rect: XYRect + startX: number + startY: number + initialW: number + initialH: number +}) => + Math.abs(params.rect.x + params.rect.width / 2 - params.startX) < EPS && + Math.abs(params.rect.y + params.rect.height / 2 - params.startY) < EPS && + Math.abs(params.rect.width - params.initialW) < EPS && + Math.abs(params.rect.height - params.initialH) < EPS diff --git a/tests/__snapshots__/should-expand-node.snap.svg b/tests/__snapshots__/should-expand-node.snap.svg index 5268ba7..f8a56ce 100644 --- a/tests/__snapshots__/should-expand-node.snap.svg +++ b/tests/__snapshots__/should-expand-node.snap.svg @@ -1,4 +1,4 @@ -