From bff8bc39bee467982de6dc9908814d16187f377e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 17 Dec 2025 21:59:32 +0530 Subject: [PATCH 1/5] Remove old grid-based gap fill solver - Remove GapFillSubSolver and all grid-based gap detection code - Remove old gap fill engine files (stepGapFill, initGapFillState, etc.) - Remove gap detection algorithms (findAllGaps, findGapsOnLayer, etc.) - Keep only utility functions (calculateCoverage, findUncoveredPoints) --- lib/solvers/rectdiff/gapfill/detection.ts | 3 - .../gapfill/detection/deduplicateGaps.ts | 28 -- .../rectdiff/gapfill/detection/findAllGaps.ts | 83 ------ .../gapfill/detection/findGapsOnLayer.ts | 100 ------- .../gapfill/detection/mergeUncoveredCells.ts | 75 ------ .../rectdiff/gapfill/engine/addPlacement.ts | 27 -- .../gapfill/engine/getGapFillProgress.ts | 42 --- .../gapfill/engine/initGapFillState.ts | 57 ---- .../rectdiff/gapfill/engine/stepGapFill.ts | 128 --------- .../rectdiff/gapfill/engine/tryExpandGap.ts | 78 ------ .../rectdiff/subsolvers/GapFillSubSolver.ts | 253 ------------------ 11 files changed, 874 deletions(-) delete mode 100644 lib/solvers/rectdiff/gapfill/detection.ts delete mode 100644 lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts delete mode 100644 lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts delete mode 100644 lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts delete mode 100644 lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts delete mode 100644 lib/solvers/rectdiff/gapfill/engine/addPlacement.ts delete mode 100644 lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts delete mode 100644 lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts delete mode 100644 lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts delete mode 100644 lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts delete mode 100644 lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts 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/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/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/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, - } - } -} From 8a0f4684e19f3393e534377fd787542fe1f0f988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 17 Dec 2025 21:59:36 +0530 Subject: [PATCH 2/5] Add BasePipelineSolver for standardized phase management - Create BasePipelineSolver abstract class - Provides standard pattern for multi-phase solvers - Handles phase transitions and step execution - Abstract methods: getCurrentPhase, setPhase, stepPhase --- lib/solvers/BasePipelineSolver.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lib/solvers/BasePipelineSolver.ts diff --git a/lib/solvers/BasePipelineSolver.ts b/lib/solvers/BasePipelineSolver.ts new file mode 100644 index 0000000..8811c5d --- /dev/null +++ b/lib/solvers/BasePipelineSolver.ts @@ -0,0 +1,30 @@ +// lib/solvers/BasePipelineSolver.ts +import { BaseSolver } from "@tscircuit/solver-utils" + +/** + * Base class for solvers that operate in distinct phases/stages. + * Each phase is a step function that returns the next phase or null if done. + */ +export abstract class BasePipelineSolver extends BaseSolver { + protected abstract getCurrentPhase(): string | null + protected abstract setPhase(phase: string | null): void + protected abstract stepPhase(phase: string): string | null + + override _step() { + const currentPhase = this.getCurrentPhase() + if (!currentPhase) { + this.solved = true + return + } + + const nextPhase = this.stepPhase(currentPhase) + this.setPhase(nextPhase) + } + + /** + * Get the current phase name for visualization/stats. + */ + getPhase(): string | null { + return this.getCurrentPhase() + } +} From 3abb73adba666338e324417cbadba107d76de93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 17 Dec 2025 21:59:39 +0530 Subject: [PATCH 3/5] Add new edge-based gap fill solver - Implement edge extraction from placed rectangles - Find nearby parallel edges within distance threshold - Detect unoccupied segments along edges - Generate expansion points outside unoccupied segments - Extremely granular stages for visualization: * SELECT_EDGE: Select primary edge * FIND_NEARBY: Find nearby parallel edges * FIND_SEGMENTS: Find unoccupied segments * GENERATE_POINTS: Generate expansion points * EXPAND_FROM_POINT: Expand rectangles from points --- lib/solvers/rectdiff/gapfill-edge/edges.ts | 148 +++++++++++ lib/solvers/rectdiff/gapfill-edge/engine.ts | 237 ++++++++++++++++++ lib/solvers/rectdiff/gapfill-edge/segments.ts | 165 ++++++++++++ lib/solvers/rectdiff/gapfill-edge/types.ts | 58 +++++ 4 files changed, 608 insertions(+) create mode 100644 lib/solvers/rectdiff/gapfill-edge/edges.ts create mode 100644 lib/solvers/rectdiff/gapfill-edge/engine.ts create mode 100644 lib/solvers/rectdiff/gapfill-edge/segments.ts create mode 100644 lib/solvers/rectdiff/gapfill-edge/types.ts diff --git a/lib/solvers/rectdiff/gapfill-edge/edges.ts b/lib/solvers/rectdiff/gapfill-edge/edges.ts new file mode 100644 index 0000000..6bc5915 --- /dev/null +++ b/lib/solvers/rectdiff/gapfill-edge/edges.ts @@ -0,0 +1,148 @@ +// lib/solvers/rectdiff/gapfill-edge/edges.ts +import type { XYRect, Placed3D } from "../types" +import type { RectEdge } from "./types" +import { EPS } from "../geometry" + +/** + * Extract all edges from a list of placed rectangles. + */ +export function extractAllEdges(placed: Placed3D[]): RectEdge[] { + const edges: RectEdge[] = [] + + for (const p of placed) { + const { rect, zLayers } = p + + // Top edge (horizontal, left to right) + edges.push({ + rect, + side: "top", + orientation: "horizontal", + start: { x: rect.x, y: rect.y }, + end: { x: rect.x + rect.width, y: rect.y }, + zLayers: [...zLayers], + }) + + // Right edge (vertical, top to bottom) + edges.push({ + rect, + side: "right", + orientation: "vertical", + start: { x: rect.x + rect.width, y: rect.y }, + end: { x: rect.x + rect.width, y: rect.y + rect.height }, + zLayers: [...zLayers], + }) + + // Bottom edge (horizontal, right to left) + edges.push({ + rect, + side: "bottom", + orientation: "horizontal", + start: { x: rect.x + rect.width, y: rect.y + rect.height }, + end: { x: rect.x, y: rect.y + rect.height }, + zLayers: [...zLayers], + }) + + // Left edge (vertical, bottom to top) + edges.push({ + rect, + side: "left", + orientation: "vertical", + start: { x: rect.x, y: rect.y + rect.height }, + end: { x: rect.x, y: rect.y }, + zLayers: [...zLayers], + }) + } + + return edges +} + +/** + * Check if two edges are parallel. + */ +export function areEdgesParallel(e1: RectEdge, e2: RectEdge): boolean { + return e1.orientation === e2.orientation +} + +/** + * Calculate the distance between two parallel edges. + * For horizontal edges, this is the vertical distance. + * For vertical edges, this is the horizontal distance. + */ +export function distanceBetweenParallelEdges( + e1: RectEdge, + e2: RectEdge, +): number { + if (!areEdgesParallel(e1, e2)) { + return Infinity + } + + if (e1.orientation === "horizontal") { + // Vertical distance between horizontal edges + return Math.abs(e1.start.y - e2.start.y) + } else { + // Horizontal distance between vertical edges + return Math.abs(e1.start.x - e2.start.x) + } +} + +/** + * Check if two parallel edges overlap in their projection. + * For horizontal edges, check x-range overlap. + * For vertical edges, check y-range overlap. + */ +export function edgesOverlapInProjection(e1: RectEdge, e2: RectEdge): boolean { + if (!areEdgesParallel(e1, e2)) { + return false + } + + if (e1.orientation === "horizontal") { + // Check x-range overlap + const e1MinX = Math.min(e1.start.x, e1.end.x) + const e1MaxX = Math.max(e1.start.x, e1.end.x) + const e2MinX = Math.min(e2.start.x, e2.end.x) + const e2MaxX = Math.max(e2.start.x, e2.end.x) + + return !(e1MaxX <= e2MinX + EPS || e2MaxX <= e1MinX + EPS) + } else { + // Check y-range overlap + const e1MinY = Math.min(e1.start.y, e1.end.y) + const e1MaxY = Math.max(e1.start.y, e1.end.y) + const e2MinY = Math.min(e2.start.y, e2.end.y) + const e2MaxY = Math.max(e2.start.y, e2.end.y) + + return !(e1MaxY <= e2MinY + EPS || e2MaxY <= e1MinY + EPS) + } +} + +/** + * Find all edges that are parallel and close to the primary edge. + */ +export function findNearbyEdges( + primaryEdge: RectEdge, + allEdges: RectEdge[], + maxDistance: number, +): RectEdge[] { + const nearby: RectEdge[] = [] + + for (const edge of allEdges) { + // Skip the primary edge itself + if (edge === primaryEdge) continue + + // Must be parallel + if (!areEdgesParallel(primaryEdge, edge)) continue + + // Must be close enough + const distance = distanceBetweenParallelEdges(primaryEdge, edge) + if (distance > maxDistance + EPS) continue + + // Should overlap in projection (or be very close) + if (!edgesOverlapInProjection(primaryEdge, edge)) { + // Allow edges that are close even if they don't overlap + if (distance > maxDistance * 0.5) continue + } + + nearby.push(edge) + } + + return nearby +} diff --git a/lib/solvers/rectdiff/gapfill-edge/engine.ts b/lib/solvers/rectdiff/gapfill-edge/engine.ts new file mode 100644 index 0000000..52f44d1 --- /dev/null +++ b/lib/solvers/rectdiff/gapfill-edge/engine.ts @@ -0,0 +1,237 @@ +// lib/solvers/rectdiff/gapfill-edge/engine.ts +import type { RectDiffState } from "../types" +import type { EdgeGapFillState } from "./types" +import { extractAllEdges } from "./edges" +import { findNearbyEdges } from "./edges" +import { findUnoccupiedSegments, generateExpansionPoints } from "./segments" +import { expandRectFromSeed } from "../geometry" +import { overlaps } from "../geometry" +import { buildHardPlacedByLayer } from "../engine" + +const DEFAULT_MAX_EDGE_DISTANCE = 2.0 +const DEFAULT_MIN_SEGMENT_LENGTH = 0.1 + +/** + * Initialize edge-based gap fill state. + */ +export function initEdgeGapFillState(state: RectDiffState): EdgeGapFillState { + const allEdges = extractAllEdges(state.placed) + + return { + bounds: state.bounds, + layerCount: state.layerCount, + obstaclesByLayer: state.obstaclesByLayer, + placed: state.placed, + placedByLayer: state.placedByLayer, + allEdges, + edgeIndex: 0, + stage: "SELECT_EDGE", + primaryEdge: null, + nearbyEdges: [], + unoccupiedSegments: [], + expansionPoints: [], + expansionPointIndex: 0, + currentExpandingRect: null, + maxEdgeDistance: DEFAULT_MAX_EDGE_DISTANCE, + minSegmentLength: DEFAULT_MIN_SEGMENT_LENGTH, + } +} + +/** + * Step the edge-based gap fill algorithm. + * Returns true if still working, false if done. + * Each step is extremely granular for visualization. + */ +export function stepEdgeGapFill( + gapFillState: EdgeGapFillState, + rectDiffState: RectDiffState, +): boolean { + switch (gapFillState.stage) { + case "SELECT_EDGE": { + // Check if we're done with all edges + if (gapFillState.edgeIndex >= gapFillState.allEdges.length) { + gapFillState.stage = "DONE" + return false + } + + // Select the next primary edge + const primaryEdge = gapFillState.allEdges[gapFillState.edgeIndex]! + gapFillState.primaryEdge = primaryEdge + gapFillState.stage = "FIND_NEARBY" + return true + } + + case "FIND_NEARBY": { + if (!gapFillState.primaryEdge) { + gapFillState.stage = "SELECT_EDGE" + return true + } + + // Find nearby edges + gapFillState.nearbyEdges = findNearbyEdges( + gapFillState.primaryEdge, + gapFillState.allEdges, + gapFillState.maxEdgeDistance, + ) + + gapFillState.stage = "FIND_SEGMENTS" + return true + } + + case "FIND_SEGMENTS": { + if (!gapFillState.primaryEdge) { + gapFillState.stage = "SELECT_EDGE" + return true + } + + // Get obstacles for the layers of this edge + const obstacles: Array<{ + x: number + y: number + width: number + height: number + }> = [] + for (const z of gapFillState.primaryEdge.zLayers) { + obstacles.push(...(gapFillState.obstaclesByLayer[z] ?? [])) + } + + // Find unoccupied segments + gapFillState.unoccupiedSegments = findUnoccupiedSegments( + gapFillState.primaryEdge, + gapFillState.nearbyEdges, + obstacles, + gapFillState.minSegmentLength, + ) + + gapFillState.stage = "GENERATE_POINTS" + return true + } + + case "GENERATE_POINTS": { + if (!gapFillState.primaryEdge) { + gapFillState.stage = "SELECT_EDGE" + return true + } + + // Generate expansion points from unoccupied segments + gapFillState.expansionPoints = generateExpansionPoints( + gapFillState.primaryEdge, + gapFillState.unoccupiedSegments, + gapFillState.primaryEdge.zLayers, + ) + + gapFillState.expansionPointIndex = 0 + + if (gapFillState.expansionPoints.length === 0) { + // No expansion points, move to next edge + gapFillState.edgeIndex++ + gapFillState.primaryEdge = null + gapFillState.nearbyEdges = [] + gapFillState.unoccupiedSegments = [] + gapFillState.stage = "SELECT_EDGE" + return true + } + + gapFillState.stage = "EXPAND_FROM_POINT" + return true + } + + case "EXPAND_FROM_POINT": { + if ( + gapFillState.expansionPointIndex >= gapFillState.expansionPoints.length + ) { + // Done with all expansion points for this edge, move to next edge + gapFillState.edgeIndex++ + gapFillState.primaryEdge = null + gapFillState.nearbyEdges = [] + gapFillState.unoccupiedSegments = [] + gapFillState.expansionPoints = [] + gapFillState.expansionPointIndex = 0 + gapFillState.currentExpandingRect = null + gapFillState.stage = "SELECT_EDGE" + return true + } + + // Try to expand from current expansion point + const point = + gapFillState.expansionPoints[gapFillState.expansionPointIndex]! + const hardPlacedByLayer = buildHardPlacedByLayer(rectDiffState) + + // Get blockers for the layers (all obstacles and hard-placed rects) + const blockers: Array<{ + x: number + y: number + width: number + height: number + }> = [] + for (const z of point.zLayers) { + const obs = rectDiffState.obstaclesByLayer[z] ?? [] + const hard = hardPlacedByLayer[z] ?? [] + blockers.push(...obs, ...hard) + } + + // Try to expand a rectangle from this point + const lastGrid = + rectDiffState.options.gridSizes[ + rectDiffState.options.gridSizes.length - 1 + ]! + const minSize = Math.min( + rectDiffState.options.minSingle.width, + rectDiffState.options.minSingle.height, + ) + + const expanded = expandRectFromSeed({ + startX: point.x, + startY: point.y, + gridSize: lastGrid, + bounds: gapFillState.bounds, + blockers, + initialCellRatio: 0.1, + maxAspectRatio: null, + minReq: { width: minSize, height: minSize }, + }) + + gapFillState.currentExpandingRect = expanded || null + + if (expanded) { + // Check if it overlaps with existing placed rects on these layers + let canPlace = true + for (const z of point.zLayers) { + const placed = gapFillState.placedByLayer[z] ?? [] + for (const p of placed) { + if (overlaps(expanded, p)) { + canPlace = false + break + } + } + if (!canPlace) break + } + + if (canPlace) { + // Place the new rectangle + const newPlacement = { rect: expanded, zLayers: [...point.zLayers] } + gapFillState.placed.push(newPlacement) + for (const z of point.zLayers) { + gapFillState.placedByLayer[z]!.push(expanded) + } + // Update rectDiffState as well + rectDiffState.placed.push(newPlacement) + for (const z of point.zLayers) { + rectDiffState.placedByLayer[z]!.push(expanded) + } + } + } + + gapFillState.expansionPointIndex++ + // Stay in EXPAND_FROM_POINT stage to process next point + return true + } + + case "DONE": + return false + + default: + gapFillState.stage = "SELECT_EDGE" + return true + } +} diff --git a/lib/solvers/rectdiff/gapfill-edge/segments.ts b/lib/solvers/rectdiff/gapfill-edge/segments.ts new file mode 100644 index 0000000..daaa9c7 --- /dev/null +++ b/lib/solvers/rectdiff/gapfill-edge/segments.ts @@ -0,0 +1,165 @@ +// lib/solvers/rectdiff/gapfill-edge/segments.ts +import type { RectEdge } from "./types" +import type { XYRect } from "../types" +import { EPS } from "../geometry" + +/** + * Find unoccupied segments along a primary edge. + * An unoccupied segment is a portion of the edge that is not covered by + * any nearby edges or obstacles. + */ +export function findUnoccupiedSegments( + primaryEdge: RectEdge, + nearbyEdges: RectEdge[], + obstacles: XYRect[], + minSegmentLength: number, +): Array<{ start: number; end: number }> { + const segments: Array<{ start: number; end: number }> = [] + + // Get the coordinate range of the primary edge + let edgeStart: number + let edgeEnd: number + + if (primaryEdge.orientation === "horizontal") { + edgeStart = Math.min(primaryEdge.start.x, primaryEdge.end.x) + edgeEnd = Math.max(primaryEdge.start.x, primaryEdge.end.x) + } else { + edgeStart = Math.min(primaryEdge.start.y, primaryEdge.end.y) + edgeEnd = Math.max(primaryEdge.start.y, primaryEdge.end.y) + } + + // Collect all covering intervals from nearby edges and obstacles + const coveringIntervals: Array<{ start: number; end: number }> = [] + + // Add intervals from nearby edges (they run parallel, so they cover the edge) + for (const edge of nearbyEdges) { + if (primaryEdge.orientation === "horizontal") { + const eStart = Math.min(edge.start.x, edge.end.x) + const eEnd = Math.max(edge.start.x, edge.end.x) + coveringIntervals.push({ start: eStart, end: eEnd }) + } else { + const eStart = Math.min(edge.start.y, edge.end.y) + const eEnd = Math.max(edge.start.y, edge.end.y) + coveringIntervals.push({ start: eStart, end: eEnd }) + } + } + + // Add intervals from obstacles that overlap with the edge + const edgeY = primaryEdge.start.y + const edgeX = primaryEdge.start.x + + for (const obstacle of obstacles) { + if (primaryEdge.orientation === "horizontal") { + // For horizontal edge, check if obstacle covers it vertically + if ( + obstacle.y <= edgeY + EPS && + obstacle.y + obstacle.height >= edgeY - EPS + ) { + const oStart = Math.max(edgeStart, obstacle.x) + const oEnd = Math.min(edgeEnd, obstacle.x + obstacle.width) + if (oEnd > oStart + EPS) { + coveringIntervals.push({ start: oStart, end: oEnd }) + } + } + } else { + // For vertical edge, check if obstacle covers it horizontally + if ( + obstacle.x <= edgeX + EPS && + obstacle.x + obstacle.width >= edgeX - EPS + ) { + const oStart = Math.max(edgeStart, obstacle.y) + const oEnd = Math.min(edgeEnd, obstacle.y + obstacle.height) + if (oEnd > oStart + EPS) { + coveringIntervals.push({ start: oStart, end: oEnd }) + } + } + } + } + + // Sort intervals by start position + coveringIntervals.sort((a, b) => a.start - b.start) + + // Merge overlapping intervals + const merged: Array<{ start: number; end: number }> = [] + for (const interval of coveringIntervals) { + if (merged.length === 0) { + merged.push({ ...interval }) + } else { + const last = merged[merged.length - 1]! + if (interval.start <= last.end + EPS) { + // Overlaps or adjacent, merge + last.end = Math.max(last.end, interval.end) + } else { + // New interval + merged.push({ ...interval }) + } + } + } + + // Find uncovered segments + let currentPos = edgeStart + + for (const interval of merged) { + if (interval.start > currentPos + EPS) { + // Found an uncovered segment + const segmentLength = interval.start - currentPos + if (segmentLength >= minSegmentLength - EPS) { + segments.push({ start: currentPos, end: interval.start }) + } + } + currentPos = Math.max(currentPos, interval.end) + } + + // Check for uncovered segment at the end + if (currentPos < edgeEnd - EPS) { + const segmentLength = edgeEnd - currentPos + if (segmentLength >= minSegmentLength - EPS) { + segments.push({ start: currentPos, end: edgeEnd }) + } + } + + return segments +} + +/** + * Generate expansion points from unoccupied segments. + * Each point is placed outside the edge (in the direction away from the rect). + */ +export function generateExpansionPoints( + primaryEdge: RectEdge, + unoccupiedSegments: Array<{ start: number; end: number }>, + zLayers: number[], + offset: number = 0.1, +): Array<{ x: number; y: number; zLayers: number[] }> { + const points: Array<{ x: number; y: number; zLayers: number[] }> = [] + + for (const segment of unoccupiedSegments) { + // Calculate the center of the segment + const segmentCenter = (segment.start + segment.end) / 2 + + let x: number + let y: number + + if (primaryEdge.orientation === "horizontal") { + x = segmentCenter + // Place point outside the edge based on which side + if (primaryEdge.side === "top") { + y = primaryEdge.start.y - offset // Above the top edge + } else { + y = primaryEdge.start.y + offset // Below the bottom edge + } + } else { + y = segmentCenter + // Place point outside the edge based on which side + if (primaryEdge.side === "right") { + x = primaryEdge.start.x + offset // To the right of the right edge + } else { + x = primaryEdge.start.x - offset // To the left of the left edge + } + } + + points.push({ x, y, zLayers: [...zLayers] }) + } + + return points +} diff --git a/lib/solvers/rectdiff/gapfill-edge/types.ts b/lib/solvers/rectdiff/gapfill-edge/types.ts new file mode 100644 index 0000000..b187000 --- /dev/null +++ b/lib/solvers/rectdiff/gapfill-edge/types.ts @@ -0,0 +1,58 @@ +// lib/solvers/rectdiff/gapfill-edge/types.ts +import type { XYRect, Placed3D } from "../types" + +export type EdgeOrientation = "horizontal" | "vertical" + +export interface RectEdge { + /** The rectangle this edge belongs to */ + rect: XYRect + /** Which edge: "top", "right", "bottom", "left" */ + side: "top" | "right" | "bottom" | "left" + /** Orientation of the edge */ + orientation: EdgeOrientation + /** Start point of the edge */ + start: { x: number; y: number } + /** End point of the edge */ + end: { x: number; y: number } + /** Z-layers where this edge exists */ + zLayers: number[] +} + +export type GapFillStage = + | "SELECT_EDGE" + | "FIND_NEARBY" + | "FIND_SEGMENTS" + | "GENERATE_POINTS" + | "EXPAND_FROM_POINT" + | "DONE" + +export interface EdgeGapFillState { + bounds: XYRect + layerCount: number + obstaclesByLayer: XYRect[][] + placed: Placed3D[] + placedByLayer: XYRect[][] + + // All edges from placed rectangles + allEdges: RectEdge[] + // Current edge index being processed + edgeIndex: number + // Current stage in the gap fill process + stage: GapFillStage + // Current primary edge being explored + primaryEdge: RectEdge | null + // Nearby edges (parallel and close to primary edge) + nearbyEdges: RectEdge[] + // Unoccupied segments of the primary edge + unoccupiedSegments: Array<{ start: number; end: number }> + // Points to expand from + expansionPoints: Array<{ x: number; y: number; zLayers: number[] }> + // Current expansion point index + expansionPointIndex: number + // Currently expanding rectangle (for visualization) + currentExpandingRect: XYRect | null + + // Options + maxEdgeDistance: number // Maximum distance for "nearby" edges + minSegmentLength: number // Minimum unoccupied segment length to consider +} From a4a3b55ace82acc82dee5734884fbba2b0d46bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 17 Dec 2025 21:59:41 +0530 Subject: [PATCH 4/5] Integrate edge-based gap fill into RectDiffSolver - Convert RectDiffSolver to use BasePipelineSolver - Integrate edge-based gap fill in GAP_FILL phase - Add comprehensive visualization for gap fill stages: * Primary edge (red), nearby edges (orange) * Unoccupied segments (green), expansion points (blue/red) * Expanding rectangles (yellow) - Export buildHardPlacedByLayer for gap fill use --- lib/solvers/RectDiffSolver.ts | 182 ++++++++++++++++++++++++++++++--- lib/solvers/rectdiff/engine.ts | 2 +- 2 files changed, 167 insertions(+), 17 deletions(-) diff --git a/lib/solvers/RectDiffSolver.ts b/lib/solvers/RectDiffSolver.ts index 34bda41..1948814 100644 --- a/lib/solvers/RectDiffSolver.ts +++ b/lib/solvers/RectDiffSolver.ts @@ -14,20 +14,26 @@ 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" +import { + initEdgeGapFillState, + stepEdgeGapFill, +} from "./rectdiff/gapfill-edge/engine" +import type { EdgeGapFillState } from "./rectdiff/gapfill-edge/types" +import { BasePipelineSolver } from "./BasePipelineSolver" /** * A streaming, one-step-per-iteration solver for capacity mesh generation. */ -export class RectDiffSolver extends BaseSolver { +export class RectDiffSolver extends BasePipelineSolver { private srj: SimpleRouteJson private gridOptions: Partial private state!: RectDiffState private _meshNodes: CapacityMeshNode[] = [] + private gapFillState: EdgeGapFillState | null = null constructor(opts: { simpleRouteJson: SimpleRouteJson @@ -46,28 +52,64 @@ export class RectDiffSolver extends BaseSolver { } } - /** Exactly ONE small step per call. */ - override _step() { - if (this.state.phase === "GRID") { - stepGrid(this.state) - } else if (this.state.phase === "EXPANSION") { - stepExpansion(this.state) - } else if (this.state.phase === "GAP_FILL") { + protected getCurrentPhase(): string | null { + if (!this.state) { + return null + } + return this.state.phase === "DONE" ? null : this.state.phase + } + + protected setPhase(phase: string | null): void { + if (!this.state) { + return + } + if (phase === null) { this.state.phase = "DONE" - } else if (this.state.phase === "DONE") { - // Finalize once + // Finalize once when done if (!this.solved) { const rects = finalizeRects(this.state) this._meshNodes = rectsToMeshNodes(rects) this.solved = true } - return + } else { + this.state.phase = phase as RectDiffState["phase"] } + } + + protected stepPhase(phase: string): string | null { + if (!this.state) { + return null + } + if (phase === "GRID") { + stepGrid(this.state) + return this.state.phase + } else if (phase === "EXPANSION") { + stepExpansion(this.state) + return this.state.phase + } else if (phase === "GAP_FILL") { + // Initialize gap fill state if needed + if (!this.gapFillState) { + this.gapFillState = initEdgeGapFillState(this.state) + } + // Step the gap fill algorithm + const stillWorking = stepEdgeGapFill(this.gapFillState, this.state) + if (!stillWorking) { + return "DONE" + } + return "GAP_FILL" + } + return null + } + + override _step() { + super._step() // Lightweight stats for debugger - this.stats.phase = this.state.phase - this.stats.gridIndex = this.state.gridIndex - this.stats.placed = this.state.placed.length + if (this.state) { + this.stats.phase = this.state.phase + this.stats.gridIndex = this.state.gridIndex + this.stats.placed = this.state.placed.length + } } /** Compute solver progress (0 to 1). */ @@ -231,6 +273,96 @@ export class RectDiffSolver extends BaseSolver { } } + // Gap fill visualization - extremely granular + if (this.state?.phase === "GAP_FILL" && this.gapFillState) { + const gf = this.gapFillState + + // Highlight primary edge (thick, bright red) - shown in all stages after SELECT_EDGE + if (gf.primaryEdge && gf.stage !== "SELECT_EDGE") { + lines.push({ + points: [gf.primaryEdge.start, gf.primaryEdge.end], + strokeColor: "#ef4444", + strokeWidth: 0.08, + label: "PRIMARY EDGE", + }) + } + + // Highlight nearby edges (orange) - shown in FIND_SEGMENTS, GENERATE_POINTS, EXPAND_FROM_POINT + if ( + gf.stage === "FIND_SEGMENTS" || + gf.stage === "GENERATE_POINTS" || + gf.stage === "EXPAND_FROM_POINT" + ) { + for (const edge of gf.nearbyEdges) { + lines.push({ + points: [edge.start, edge.end], + strokeColor: "#f59e0b", + strokeWidth: 0.04, + label: "nearby", + }) + } + } + + // Highlight unoccupied segments (green) - shown in GENERATE_POINTS and EXPAND_FROM_POINT + if ( + gf.primaryEdge && + (gf.stage === "GENERATE_POINTS" || gf.stage === "EXPAND_FROM_POINT") && + gf.unoccupiedSegments.length > 0 + ) { + for (const segment of gf.unoccupiedSegments) { + let start: { x: number; y: number } + let end: { x: number; y: number } + + if (gf.primaryEdge.orientation === "horizontal") { + start = { x: segment.start, y: gf.primaryEdge.start.y } + end = { x: segment.end, y: gf.primaryEdge.start.y } + } else { + start = { x: gf.primaryEdge.start.x, y: segment.start } + end = { x: gf.primaryEdge.start.x, y: segment.end } + } + + lines.push({ + points: [start, end], + strokeColor: "#10b981", + strokeWidth: 0.06, + label: "unoccupied", + }) + } + } + + // Show expansion points (blue) - shown in EXPAND_FROM_POINT + if (gf.stage === "EXPAND_FROM_POINT") { + for (let i = 0; i < gf.expansionPoints.length; i++) { + const point = gf.expansionPoints[i]! + const isCurrent = i === gf.expansionPointIndex + points.push({ + x: point.x, + y: point.y, + fill: isCurrent ? "#dc2626" : "#3b82f6", + stroke: isCurrent ? "#991b1b" : "#1e40af", + label: isCurrent + ? `EXPANDING\nz:${point.zLayers.join(",")}` + : `point\nz:${point.zLayers.join(",")}`, + } as any) + } + + // Show currently expanding rectangle (yellow) + if (gf.currentExpandingRect) { + rects.push({ + center: { + x: gf.currentExpandingRect.x + gf.currentExpandingRect.width / 2, + y: gf.currentExpandingRect.y + gf.currentExpandingRect.height / 2, + }, + width: gf.currentExpandingRect.width, + height: gf.currentExpandingRect.height, + fill: "#fef3c7", + stroke: "#f59e0b", + label: "expanding", + }) + } + } + } + // current placements (streaming) if not yet solved if (this.state?.placed?.length) { for (const p of this.state.placed) { @@ -249,8 +381,26 @@ export class RectDiffSolver extends BaseSolver { } } + let phaseTitle = `RectDiff (${this.state?.phase ?? "init"})` + if (this.state?.phase === "GAP_FILL" && this.gapFillState) { + const gf = this.gapFillState + const stageLabels: Record = { + SELECT_EDGE: "Selecting Edge", + FIND_NEARBY: "Finding Nearby", + FIND_SEGMENTS: "Finding Segments", + GENERATE_POINTS: "Generating Points", + EXPAND_FROM_POINT: "Expanding", + DONE: "Done", + } + phaseTitle = `GapFill: ${stageLabels[gf.stage] ?? gf.stage} (edge ${gf.edgeIndex + 1}/${gf.allEdges.length}${ + gf.expansionPoints.length > 0 + ? `, point ${gf.expansionPointIndex + 1}/${gf.expansionPoints.length}` + : "" + })` + } + return { - title: `RectDiff (${this.state?.phase ?? "init"})`, + title: phaseTitle, coordinateSystem: "cartesian", rects, points, diff --git a/lib/solvers/rectdiff/engine.ts b/lib/solvers/rectdiff/engine.ts index 963625b..22ec475 100644 --- a/lib/solvers/rectdiff/engine.ts +++ b/lib/solvers/rectdiff/engine.ts @@ -128,7 +128,7 @@ export function initState( /** * Build per-layer list of "hard" placed rects (nodes spanning all layers). */ -function buildHardPlacedByLayer(state: RectDiffState): XYRect[][] { +export function buildHardPlacedByLayer(state: RectDiffState): XYRect[][] { const out: XYRect[][] = Array.from({ length: state.layerCount }, () => []) for (const p of state.placed) { if (p.zLayers.length >= state.layerCount) { From 9c599e69a862db302695897c7fab43c5d5d6c3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Wed, 17 Dec 2025 21:59:44 +0530 Subject: [PATCH 5/5] Clean up gapfill module - remove unused types - Remove GapFillOptions, GapRegion, GapFillState types - Keep only LayerContext for utility functions - Update engine exports to only include utility functions --- lib/solvers/rectdiff/gapfill/engine.ts | 5 +-- lib/solvers/rectdiff/gapfill/types.ts | 57 +++----------------------- package.json | 2 +- 3 files changed, 7 insertions(+), 57 deletions(-) diff --git a/lib/solvers/rectdiff/gapfill/engine.ts b/lib/solvers/rectdiff/gapfill/engine.ts index a440663..202c491 100644 --- a/lib/solvers/rectdiff/gapfill/engine.ts +++ b/lib/solvers/rectdiff/gapfill/engine.ts @@ -1,7 +1,4 @@ // lib/solvers/rectdiff/gapfill/engine.ts +// Utility functions for coverage analysis (still used by RectDiffSolver) 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/types.ts b/lib/solvers/rectdiff/gapfill/types.ts index 59bdbf6..cc884f8 100644 --- a/lib/solvers/rectdiff/gapfill/types.ts +++ b/lib/solvers/rectdiff/gapfill/types.ts @@ -1,57 +1,10 @@ // lib/solvers/rectdiff/gapfill/types.ts -import type { XYRect, Placed3D } from "../types" +import type { XYRect } 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 */ +/** + * Context for layer-based operations shared across utility functions. + * Used by calculateCoverage and findUncoveredPoints. + */ export interface LayerContext { bounds: XYRect layerCount: number diff --git a/package.json b/package.json index 5170dec..8e62a2a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@biomejs/biome": "^2.3.5", "@react-hook/resize-observer": "^2.0.2", - "@tscircuit/solver-utils": "^0.0.3", + "@tscircuit/solver-utils": "^0.0.4", "@types/bun": "latest", "@types/react": "^18", "@types/react-dom": "^18",