diff --git a/lib/solvers/RectDiffSolver.ts b/lib/solvers/RectDiffSolver.ts index 34bda41..ad85b69 100644 --- a/lib/solvers/RectDiffSolver.ts +++ b/lib/solvers/RectDiffSolver.ts @@ -14,11 +14,6 @@ import { } from "./rectdiff/engine" import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes" import { overlaps } from "./rectdiff/geometry" -import type { GapFillOptions } from "./rectdiff/gapfill/types" -import { - findUncoveredPoints, - calculateCoverage, -} from "./rectdiff/gapfill/engine" /** * A streaming, one-step-per-iteration solver for capacity mesh generation. @@ -82,34 +77,6 @@ export class RectDiffSolver extends BaseSolver { return { meshNodes: this._meshNodes } } - /** Get coverage percentage (0-1). */ - getCoverage(sampleResolution: number = 0.05): number { - return calculateCoverage( - { sampleResolution }, - { - bounds: this.state.bounds, - layerCount: this.state.layerCount, - obstaclesByLayer: this.state.obstaclesByLayer, - placedByLayer: this.state.placedByLayer, - }, - ) - } - - /** Find uncovered points for debugging gaps. */ - getUncoveredPoints( - sampleResolution: number = 0.05, - ): Array<{ x: number; y: number; z: number }> { - return findUncoveredPoints( - { sampleResolution }, - { - bounds: this.state.bounds, - layerCount: this.state.layerCount, - obstaclesByLayer: this.state.obstaclesByLayer, - placedByLayer: this.state.placedByLayer, - }, - ) - } - /** Get color based on z layer for visualization. */ private getColorForZLayer(zLayers: number[]): { fill: string diff --git a/lib/solvers/rectdiff/gapfill/detection.ts b/lib/solvers/rectdiff/gapfill/detection.ts deleted file mode 100644 index 7126d89..0000000 --- a/lib/solvers/rectdiff/gapfill/detection.ts +++ /dev/null @@ -1,3 +0,0 @@ -// lib/solvers/rectdiff/gapfill/detection.ts -export * from "./detection/findAllGaps" -// findGapsOnLayer is not exported as it's only used by findAllGaps diff --git a/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts b/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts deleted file mode 100644 index 0f45164..0000000 --- a/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +++ /dev/null @@ -1,28 +0,0 @@ -// lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts -import { rectsEqual } from "../../../../../utils/rectsEqual" -import { rectsOverlap } from "../../../../../utils/rectsOverlap" -import type { GapRegion } from "../types" - -export function deduplicateGaps(gaps: GapRegion[]): GapRegion[] { - const result: GapRegion[] = [] - - for (const gap of gaps) { - // Check if we already have a gap at the same location with overlapping layers - const existing = result.find( - (g) => - rectsEqual(g.rect, gap.rect) || - (rectsOverlap(g.rect, gap.rect) && - gap.zLayers.some((z) => g.zLayers.includes(z))), - ) - - if (!existing) { - result.push(gap) - } else if (gap.zLayers.length > existing.zLayers.length) { - // Replace with the one that has more layers - const idx = result.indexOf(existing) - result[idx] = gap - } - } - - return result -} diff --git a/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts b/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts deleted file mode 100644 index d4cc9ec..0000000 --- a/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +++ /dev/null @@ -1,83 +0,0 @@ -// lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts -import type { XYRect } from "../../types" -import type { GapRegion, LayerContext } from "../types" -import { EPS } from "../../geometry" -import { findGapsOnLayer } from "./findGapsOnLayer" -import { rectsOverlap } from "../../../../../utils/rectsOverlap" -import { deduplicateGaps } from "./deduplicateGaps" - -/** - * Find gaps across all layers and return GapRegions with z-layer info. - */ -export function findAllGaps( - { - scanResolution, - minWidth, - minHeight, - }: { - scanResolution: number - minWidth: number - minHeight: number - }, - ctx: LayerContext, -): GapRegion[] { - const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx - - // Find gaps on each layer - const gapsByLayer: XYRect[][] = [] - for (let z = 0; z < layerCount; z++) { - const obstacles = obstaclesByLayer[z] ?? [] - const placed = placedByLayer[z] ?? [] - const gaps = findGapsOnLayer({ bounds, obstacles, placed, scanResolution }) - gapsByLayer.push(gaps) - } - - // Convert to GapRegions with z-layer info - const allGaps: GapRegion[] = [] - - for (let z = 0; z < layerCount; z++) { - for (const gap of gapsByLayer[z]!) { - // Filter out gaps that are too small - if (gap.width < minWidth - EPS || gap.height < minHeight - EPS) continue - - // Check if this gap exists on adjacent layers too - const zLayers = [z] - - // Look up - for (let zu = z + 1; zu < layerCount; zu++) { - const hasOverlap = gapsByLayer[zu]!.some((g) => rectsOverlap(g, gap)) - if (hasOverlap) zLayers.push(zu) - else break - } - - // Look down (if z > 0 and not already counted) - for (let zd = z - 1; zd >= 0; zd--) { - const hasOverlap = gapsByLayer[zd]!.some((g) => rectsOverlap(g, gap)) - if (hasOverlap && !zLayers.includes(zd)) zLayers.unshift(zd) - else break - } - - allGaps.push({ - rect: gap, - zLayers: zLayers.sort((a, b) => a - b), - centerX: gap.x + gap.width / 2, - centerY: gap.y + gap.height / 2, - area: gap.width * gap.height, - }) - } - } - - // Deduplicate gaps that are essentially the same across layers - const deduped = deduplicateGaps(allGaps) - - // Sort by priority: prefer larger gaps and multi-layer gaps - deduped.sort((a, b) => { - // Prefer multi-layer gaps - const layerDiff = b.zLayers.length - a.zLayers.length - if (layerDiff !== 0) return layerDiff - // Then prefer larger area - return b.area - a.area - }) - - return deduped -} diff --git a/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts b/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts deleted file mode 100644 index 895ed9d..0000000 --- a/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +++ /dev/null @@ -1,100 +0,0 @@ -// lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts -import type { XYRect } from "../../types" -import { EPS } from "../../geometry" - -import { mergeUncoveredCells } from "./mergeUncoveredCells" - -/** - * Sweep-line algorithm to find maximal uncovered rectangles on a single layer. - */ -export function findGapsOnLayer({ - bounds, - obstacles, - placed, - scanResolution, -}: { - bounds: XYRect - obstacles: XYRect[] - placed: XYRect[] - scanResolution: number -}): XYRect[] { - const blockers = [...obstacles, ...placed] - - // Collect all unique x-coordinates - const xCoords = new Set() - xCoords.add(bounds.x) - xCoords.add(bounds.x + bounds.width) - - for (const b of blockers) { - if (b.x > bounds.x && b.x < bounds.x + bounds.width) { - xCoords.add(b.x) - } - if (b.x + b.width > bounds.x && b.x + b.width < bounds.x + bounds.width) { - xCoords.add(b.x + b.width) - } - } - - // Also add intermediate points based on scan resolution - for (let x = bounds.x; x <= bounds.x + bounds.width; x += scanResolution) { - xCoords.add(x) - } - - const sortedX = Array.from(xCoords).sort((a, b) => a - b) - - // Similarly for y-coordinates - const yCoords = new Set() - yCoords.add(bounds.y) - yCoords.add(bounds.y + bounds.height) - - for (const b of blockers) { - if (b.y > bounds.y && b.y < bounds.y + bounds.height) { - yCoords.add(b.y) - } - if ( - b.y + b.height > bounds.y && - b.y + b.height < bounds.y + bounds.height - ) { - yCoords.add(b.y + b.height) - } - } - - for (let y = bounds.y; y <= bounds.y + bounds.height; y += scanResolution) { - yCoords.add(y) - } - - const sortedY = Array.from(yCoords).sort((a, b) => a - b) - - // Build a grid of cells and mark which are uncovered - const uncoveredCells: Array<{ x: number; y: number; w: number; h: number }> = - [] - - for (let i = 0; i < sortedX.length - 1; i++) { - for (let j = 0; j < sortedY.length - 1; j++) { - const cellX = sortedX[i]! - const cellY = sortedY[j]! - const cellW = sortedX[i + 1]! - cellX - const cellH = sortedY[j + 1]! - cellY - - if (cellW <= EPS || cellH <= EPS) continue - - // Check if this cell is covered by any blocker - const cellCenterX = cellX + cellW / 2 - const cellCenterY = cellY + cellH / 2 - - const isCovered = blockers.some( - (b) => - cellCenterX >= b.x - EPS && - cellCenterX <= b.x + b.width + EPS && - cellCenterY >= b.y - EPS && - cellCenterY <= b.y + b.height + EPS, - ) - - if (!isCovered) { - uncoveredCells.push({ x: cellX, y: cellY, w: cellW, h: cellH }) - } - } - } - - // Merge adjacent uncovered cells into maximal rectangles - return mergeUncoveredCells(uncoveredCells) -} diff --git a/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts b/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts deleted file mode 100644 index 940c75c..0000000 --- a/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +++ /dev/null @@ -1,75 +0,0 @@ -// lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts -import type { XYRect } from "../../types" -import { EPS } from "../../geometry" - -/** - * Merge adjacent uncovered cells into larger rectangles using a greedy approach. - */ -export function mergeUncoveredCells( - cells: Array<{ x: number; y: number; w: number; h: number }>, -): XYRect[] { - if (cells.length === 0) return [] - - // Group cells by their left edge and width - const byXW = new Map() - for (const c of cells) { - const key = `${c.x.toFixed(9)}|${c.w.toFixed(9)}` - const arr = byXW.get(key) ?? [] - arr.push(c) - byXW.set(key, arr) - } - - // Within each vertical strip, merge adjacent cells - const verticalStrips: XYRect[] = [] - for (const stripCells of byXW.values()) { - // Sort by y - stripCells.sort((a, b) => a.y - b.y) - - let current: XYRect | null = null - for (const c of stripCells) { - if (!current) { - current = { x: c.x, y: c.y, width: c.w, height: c.h } - } else if (Math.abs(current.y + current.height - c.y) < EPS) { - // Adjacent vertically, merge - current.height += c.h - } else { - // Gap, save current and start new - verticalStrips.push(current) - current = { x: c.x, y: c.y, width: c.w, height: c.h } - } - } - if (current) verticalStrips.push(current) - } - - // Now try to merge horizontal strips with same y and height - const byYH = new Map() - for (const r of verticalStrips) { - const key = `${r.y.toFixed(9)}|${r.height.toFixed(9)}` - const arr = byYH.get(key) ?? [] - arr.push(r) - byYH.set(key, arr) - } - - const merged: XYRect[] = [] - for (const rowRects of byYH.values()) { - // Sort by x - rowRects.sort((a, b) => a.x - b.x) - - let current: XYRect | null = null - for (const r of rowRects) { - if (!current) { - current = { ...r } - } else if (Math.abs(current.x + current.width - r.x) < EPS) { - // Adjacent horizontally, merge - current.width += r.width - } else { - // Gap, save current and start new - merged.push(current) - current = { ...r } - } - } - if (current) merged.push(current) - } - - return merged -} diff --git a/lib/solvers/rectdiff/gapfill/engine.ts b/lib/solvers/rectdiff/gapfill/engine.ts deleted file mode 100644 index a440663..0000000 --- a/lib/solvers/rectdiff/gapfill/engine.ts +++ /dev/null @@ -1,7 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine.ts -export * from "./engine/calculateCoverage" -export * from "./engine/findUncoveredPoints" -export * from "./engine/getGapFillProgress" -export * from "./engine/initGapFillState" - -export * from "./engine/stepGapFill" diff --git a/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts b/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts deleted file mode 100644 index 9c7bda7..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +++ /dev/null @@ -1,27 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/addPlacement.ts -import type { Placed3D, XYRect } from "../../types" -import type { GapFillState } from "../types" - -/** - * Add a new placement to the state. - */ -export function addPlacement( - state: GapFillState, - { - rect, - zLayers, - }: { - rect: XYRect - zLayers: number[] - }, -): void { - const placed: Placed3D = { rect, zLayers: [...zLayers] } - state.placed.push(placed) - - for (const z of zLayers) { - if (!state.placedByLayer[z]) { - state.placedByLayer[z] = [] - } - state.placedByLayer[z]!.push(rect) - } -} diff --git a/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts b/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts deleted file mode 100644 index d9de4b3..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +++ /dev/null @@ -1,44 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts -import type { LayerContext } from "../types" - -/** - * Calculate coverage percentage (0-1). - */ -export function calculateCoverage( - { sampleResolution = 0.1 }: { sampleResolution?: number }, - ctx: LayerContext, -): number { - const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx - - let totalPoints = 0 - let coveredPoints = 0 - - for (let z = 0; z < layerCount; z++) { - const obstacles = obstaclesByLayer[z] ?? [] - const placed = placedByLayer[z] ?? [] - const allRects = [...obstacles, ...placed] - - for ( - let x = bounds.x; - x <= bounds.x + bounds.width; - x += sampleResolution - ) { - for ( - let y = bounds.y; - y <= bounds.y + bounds.height; - y += sampleResolution - ) { - totalPoints++ - - const isCovered = allRects.some( - (r) => - x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height, - ) - - if (isCovered) coveredPoints++ - } - } - } - - return totalPoints > 0 ? coveredPoints / totalPoints : 1 -} diff --git a/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts b/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts deleted file mode 100644 index c0a09b6..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +++ /dev/null @@ -1,43 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts -import type { LayerContext } from "../types" - -/** - * Find uncovered points for debugging gaps. - */ -export function findUncoveredPoints( - { sampleResolution = 0.05 }: { sampleResolution?: number }, - ctx: LayerContext, -): Array<{ x: number; y: number; z: number }> { - const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx - - const uncovered: Array<{ x: number; y: number; z: number }> = [] - - for (let z = 0; z < layerCount; z++) { - const obstacles = obstaclesByLayer[z] ?? [] - const placed = placedByLayer[z] ?? [] - const allRects = [...obstacles, ...placed] - - for ( - let x = bounds.x; - x <= bounds.x + bounds.width; - x += sampleResolution - ) { - for ( - let y = bounds.y; - y <= bounds.y + bounds.height; - y += sampleResolution - ) { - const isCovered = allRects.some( - (r) => - x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height, - ) - - if (!isCovered) { - uncovered.push({ x, y, z }) - } - } - } - } - - return uncovered -} diff --git a/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts b/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts deleted file mode 100644 index 6d4d65a..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +++ /dev/null @@ -1,42 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts -import type { GapFillState } from "../types" - -/** - * Get progress as a number between 0 and 1. - * Accounts for four-stage processing (scan → select → expand → place for each gap). - */ -export function getGapFillProgress(state: GapFillState): number { - if (state.done) return 1 - - const iterationProgress = state.iteration / state.options.maxIterations - const gapProgress = - state.gapsFound.length > 0 ? state.gapIndex / state.gapsFound.length : 0 - - // Add sub-progress within current gap based on stage - let stageProgress = 0 - switch (state.stage) { - case "scan": - stageProgress = 0 - break - case "select": - stageProgress = 0.25 - break - case "expand": - stageProgress = 0.5 - break - case "place": - stageProgress = 0.75 - break - } - - const gapStageProgress = - state.gapsFound.length > 0 - ? stageProgress / (state.gapsFound.length * 4) // 4 stages per gap - : 0 - - return Math.min( - 0.999, - iterationProgress + - (gapProgress + gapStageProgress) / state.options.maxIterations, - ) -} diff --git a/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts b/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts deleted file mode 100644 index 8f5d886..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +++ /dev/null @@ -1,57 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts -import type { Placed3D } from "../../types" -import type { GapFillState, GapFillOptions, LayerContext } from "../types" - -const DEFAULT_OPTIONS: GapFillOptions = { - minWidth: 0.1, - minHeight: 0.1, - maxIterations: 10, - targetCoverage: 0.999, - scanResolution: 0.5, -} - -/** - * Initialize the gap fill state from existing rectdiff state. - */ -export function initGapFillState( - { - placed, - options, - }: { - placed: Placed3D[] - options?: Partial - }, - ctx: LayerContext, -): GapFillState { - const opts = { ...DEFAULT_OPTIONS, ...options } - - // Deep copy placed arrays to avoid mutation issues - const placedCopy = placed.map((p) => ({ - rect: { ...p.rect }, - zLayers: [...p.zLayers], - })) - - const placedByLayerCopy = ctx.placedByLayer.map((layer) => - layer.map((r) => ({ ...r })), - ) - - return { - bounds: { ...ctx.bounds }, - layerCount: ctx.layerCount, - obstaclesByLayer: ctx.obstaclesByLayer, - placed: placedCopy, - placedByLayer: placedByLayerCopy, - options: opts, - iteration: 0, - gapsFound: [], - gapIndex: 0, - done: false, - initialGapCount: 0, - filledCount: 0, - // Four-stage visualization state - stage: "scan", - currentGap: null, - currentSeed: null, - expandedRect: null, - } -} diff --git a/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts b/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts deleted file mode 100644 index ad143c8..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +++ /dev/null @@ -1,128 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts -import type { GapFillState } from "../types" -import { findAllGaps } from "../detection" -import { tryExpandGap } from "./tryExpandGap" -import { addPlacement } from "./addPlacement" - -/** - * Perform one step of gap filling with four-stage visualization. - * Stages: scan → select → expand → place - * Returns true if still working, false if done. - */ -export function stepGapFill(state: GapFillState): boolean { - if (state.done) return false - - switch (state.stage) { - case "scan": { - // Stage 1: Gap detection/scanning - - // Check if we need to find new gaps - if ( - state.gapsFound.length === 0 || - state.gapIndex >= state.gapsFound.length - ) { - // Check if we've hit max iterations - if (state.iteration >= state.options.maxIterations) { - state.done = true - return false - } - - // Find new gaps - state.gapsFound = findAllGaps( - { - scanResolution: state.options.scanResolution, - minWidth: state.options.minWidth, - minHeight: state.options.minHeight, - }, - { - bounds: state.bounds, - layerCount: state.layerCount, - obstaclesByLayer: state.obstaclesByLayer, - placedByLayer: state.placedByLayer, - }, - ) - - if (state.iteration === 0) { - state.initialGapCount = state.gapsFound.length - } - - state.gapIndex = 0 - state.iteration++ - - // If no gaps found, we're done - if (state.gapsFound.length === 0) { - state.done = true - return false - } - } - - // Move to select stage - state.stage = "select" - return true - } - - case "select": { - // Stage 2: Show the gap being targeted - if (state.gapIndex >= state.gapsFound.length) { - // No more gaps in this iteration, go back to scan - state.stage = "scan" - return true - } - - state.currentGap = state.gapsFound[state.gapIndex]! - state.currentSeed = { - x: state.currentGap.centerX, - y: state.currentGap.centerY, - } - state.expandedRect = null - - // Move to expand stage - state.stage = "expand" - return true - } - - case "expand": { - // Stage 3: Show expansion attempt - if (!state.currentGap) { - // Shouldn't happen, but handle gracefully - state.stage = "select" - return true - } - - // Try to expand from the current seed - const expandedRect = tryExpandGap(state, { - gap: state.currentGap, - seed: state.currentSeed!, - }) - state.expandedRect = expandedRect - - // Move to place stage - state.stage = "place" - return true - } - - case "place": { - // Stage 4: Show the placed result - if (state.expandedRect && state.currentGap) { - // Actually place the rectangle - addPlacement(state, { - rect: state.expandedRect, - zLayers: state.currentGap.zLayers, - }) - state.filledCount++ - } - - // Move to next gap and reset to select stage - state.gapIndex++ - state.currentGap = null - state.currentSeed = null - state.expandedRect = null - state.stage = "select" - return true - } - - default: - state.stage = "scan" - return true - } -} diff --git a/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts b/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts deleted file mode 100644 index d3ae73c..0000000 --- a/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +++ /dev/null @@ -1,78 +0,0 @@ -// lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts -import type { XYRect } from "../../types" -import type { GapFillState, GapRegion } from "../types" -import { expandRectFromSeed } from "../../geometry" - -/** - * Try to expand a rectangle from a seed point within the gap. - * Returns the expanded rectangle or null if expansion fails. - */ -export function tryExpandGap( - state: GapFillState, - { - gap, - seed, - }: { - gap: GapRegion - seed: { x: number; y: number } - }, -): XYRect | null { - // Build blockers for the gap's z-layers - const blockers: XYRect[] = [] - for (const z of gap.zLayers) { - blockers.push(...(state.obstaclesByLayer[z] ?? [])) - blockers.push(...(state.placedByLayer[z] ?? [])) - } - - // Try to expand from the seed point - const rect = expandRectFromSeed({ - startX: seed.x, - startY: seed.y, - gridSize: Math.min(gap.rect.width, gap.rect.height), - bounds: state.bounds, - blockers, - initialCellRatio: 0, - maxAspectRatio: null, - minReq: { width: state.options.minWidth, height: state.options.minHeight }, - }) - - if (!rect) { - // Try additional seed points within the gap - const seeds = [ - { x: gap.rect.x + state.options.minWidth / 2, y: gap.centerY }, - { - x: gap.rect.x + gap.rect.width - state.options.minWidth / 2, - y: gap.centerY, - }, - { x: gap.centerX, y: gap.rect.y + state.options.minHeight / 2 }, - { - x: gap.centerX, - y: gap.rect.y + gap.rect.height - state.options.minHeight / 2, - }, - ] - - for (const altSeed of seeds) { - const altRect = expandRectFromSeed({ - startX: altSeed.x, - startY: altSeed.y, - gridSize: Math.min(gap.rect.width, gap.rect.height), - bounds: state.bounds, - blockers, - initialCellRatio: 0, - maxAspectRatio: null, - minReq: { - width: state.options.minWidth, - height: state.options.minHeight, - }, - }) - - if (altRect) { - return altRect - } - } - - return null - } - - return rect -} diff --git a/lib/solvers/rectdiff/gapfill/types.ts b/lib/solvers/rectdiff/gapfill/types.ts deleted file mode 100644 index 59bdbf6..0000000 --- a/lib/solvers/rectdiff/gapfill/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -// lib/solvers/rectdiff/gapfill/types.ts -import type { XYRect, Placed3D } from "../types" - -export interface GapFillOptions { - /** Minimum width for gap-fill rectangles (can be smaller than main solver) */ - minWidth: number - /** Minimum height for gap-fill rectangles */ - minHeight: number - /** Maximum iterations to prevent infinite loops */ - maxIterations: number - /** Target coverage percentage (0-1) to stop early */ - targetCoverage: number - /** Grid resolution for gap detection */ - scanResolution: number -} - -export interface GapRegion { - /** Bounding box of the gap */ - rect: XYRect - /** Z-layers where this gap exists */ - zLayers: number[] - /** Center point for seeding */ - centerX: number - centerY: number - /** Approximate area of the gap */ - area: number -} - -export interface GapFillState { - bounds: XYRect - layerCount: number - obstaclesByLayer: XYRect[][] - placed: Placed3D[] - placedByLayer: XYRect[][] - options: GapFillOptions - - // Progress tracking - iteration: number - gapsFound: GapRegion[] - gapIndex: number - done: boolean - - // Stats - initialGapCount: number - filledCount: number - - // Four-stage visualization state - stage: "scan" | "select" | "expand" | "place" - currentGap: GapRegion | null - currentSeed: { x: number; y: number } | null - expandedRect: XYRect | null -} - -/** Context for layer-based operations shared across gap fill functions */ -export interface LayerContext { - bounds: XYRect - layerCount: number - obstaclesByLayer: XYRect[][] - placedByLayer: XYRect[][] -} diff --git a/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts b/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts deleted file mode 100644 index ecc6cdc..0000000 --- a/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +++ /dev/null @@ -1,253 +0,0 @@ -// lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts -import { BaseSolver } from "@tscircuit/solver-utils" -import type { GraphicsObject } from "graphics-debug" -import type { XYRect, Placed3D } from "../types" -import type { - GapFillState, - GapFillOptions, - LayerContext, -} from "../gapfill/types" -import { - initGapFillState, - stepGapFill, - getGapFillProgress, -} from "../gapfill/engine" - -/** - * A sub-solver that fills empty spaces (gaps) left by the main grid-based - * placement algorithm. - * - * The preceding grid-based placement is fast but can leave irregular un-placed - * areas. This solver maximizes board coverage by finding and filling these - * gaps, which is critical for producing a high-quality capacity mesh. - * - * The core of the algorithm is its gap-detection phase. It works by first - * collecting all unique x and y-coordinates from the edges of existing - * obstacles and placed rectangles. This set of coordinates is supplemented by a - * uniform grid based on the `scanResolution` parameter. Together, these form a - * non-uniform grid of cells. The solver then tests the center of each cell for - * coverage. Contiguous uncovered cells are merged into larger, maximal - * rectangles, which become the candidate gaps to be filled. - * - * Once a prioritized list of gaps is generated (favoring larger, multi-layer - * gaps), the solver iteratively attempts to fill each one by expanding a new - * rectangle from a seed point until it collides with an existing boundary. - * - * The time complexity is dominated by the gap detection, which is approximately - * O((N+1/R)^2 * B), where N is the number of objects, R is the scan - * resolution, and B is the number of blockers. The algorithm's performance is - * therefore highly dependent on the `scanResolution`. It is a heuristic - * designed to be "fast enough" by avoiding a brute-force search, instead - * relying on this grid-based cell checking to find significant gaps. - */ -export class GapFillSubSolver extends BaseSolver { - private state: GapFillState - private layerCtx: LayerContext - - constructor(params: { - placed: Placed3D[] - options?: Partial - layerCtx: LayerContext - }) { - super() - this.layerCtx = params.layerCtx - this.state = initGapFillState( - { - placed: params.placed, - options: params.options, - }, - params.layerCtx, - ) - } - - /** - * Execute one step of the gap fill algorithm. - * Each gap goes through four stages: scan for gaps, select a target gap, - * expand a rectangle from seed point, then place the final result. - */ - override _step() { - const stillWorking = stepGapFill(this.state) - if (!stillWorking) { - this.solved = true - } - } - - /** - * Calculate progress as a value between 0 and 1. - * Accounts for iterations, gaps processed, and current stage within each gap. - */ - computeProgress(): number { - return getGapFillProgress(this.state) - } - - /** - * Get all placed rectangles including original ones plus newly created gap-fill rectangles. - */ - getPlaced(): Placed3D[] { - return this.state.placed - } - - /** - * Get placed rectangles organized by Z-layer for efficient layer-based operations. - */ - getPlacedByLayer(): XYRect[][] { - return this.state.placedByLayer - } - - override getOutput() { - return { - placed: this.state.placed, - placedByLayer: this.state.placedByLayer, - filledCount: this.state.filledCount, - } - } - - /** Zen visualization: show four-stage gap filling process. */ - override visualize(): GraphicsObject { - const rects: NonNullable = [] - const points: NonNullable = [] - - // Board bounds (subtle) - rects.push({ - center: { - x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2, - y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2, - }, - width: this.layerCtx.bounds.width, - height: this.layerCtx.bounds.height, - fill: "none", - stroke: "#e5e7eb", - label: "", - }) - - switch (this.state.stage) { - case "scan": { - // Stage 1: Show scanning/detection phase with light blue overlay - rects.push({ - center: { - x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2, - y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2, - }, - width: this.layerCtx.bounds.width, - height: this.layerCtx.bounds.height, - fill: "#dbeafe", - stroke: "#3b82f6", - label: "scanning", - }) - break - } - - case "select": { - // Stage 2: Show the gap being targeted (red outline) - if (this.state.currentGap) { - rects.push({ - center: { - x: - this.state.currentGap.rect.x + - this.state.currentGap.rect.width / 2, - y: - this.state.currentGap.rect.y + - this.state.currentGap.rect.height / 2, - }, - width: this.state.currentGap.rect.width, - height: this.state.currentGap.rect.height, - fill: "#fecaca", - stroke: "#ef4444", - label: "target gap", - }) - - // Show the seed point - if (this.state.currentSeed) { - points.push({ - x: this.state.currentSeed.x, - y: this.state.currentSeed.y, - color: "#dc2626", - label: "seed", - }) - } - } - break - } - - case "expand": { - // Stage 3: Show expansion attempt (yellow growing rectangle + seed) - if (this.state.currentGap) { - // Show gap outline (faded) - rects.push({ - center: { - x: - this.state.currentGap.rect.x + - this.state.currentGap.rect.width / 2, - y: - this.state.currentGap.rect.y + - this.state.currentGap.rect.height / 2, - }, - width: this.state.currentGap.rect.width, - height: this.state.currentGap.rect.height, - fill: "none", - stroke: "#f87171", - label: "", - }) - } - - if (this.state.currentSeed) { - // Show seed point - points.push({ - x: this.state.currentSeed.x, - y: this.state.currentSeed.y, - color: "#f59e0b", - label: "expanding", - }) - } - - if (this.state.expandedRect) { - // Show expanded rectangle - rects.push({ - center: { - x: this.state.expandedRect.x + this.state.expandedRect.width / 2, - y: this.state.expandedRect.y + this.state.expandedRect.height / 2, - }, - width: this.state.expandedRect.width, - height: this.state.expandedRect.height, - fill: "#fef3c7", - stroke: "#f59e0b", - label: "expanding", - }) - } - break - } - - case "place": { - // Stage 4: Show final placed rectangle (green) - if (this.state.expandedRect) { - rects.push({ - center: { - x: this.state.expandedRect.x + this.state.expandedRect.width / 2, - y: this.state.expandedRect.y + this.state.expandedRect.height / 2, - }, - width: this.state.expandedRect.width, - height: this.state.expandedRect.height, - fill: "#bbf7d0", - stroke: "#22c55e", - label: "placed", - }) - } - break - } - } - - const stageNames = { - scan: "scanning", - select: "selecting", - expand: "expanding", - place: "placing", - } - - return { - title: `GapFill (${stageNames[this.state.stage]}): ${this.state.filledCount} filled`, - coordinateSystem: "cartesian", - rects, - points, - } - } -}