@tscircuit/rectdiff 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -107,14 +107,6 @@ declare class RectDiffSolver extends BaseSolver {
107
107
  getOutput(): {
108
108
  meshNodes: CapacityMeshNode[];
109
109
  };
110
- /** Get coverage percentage (0-1). */
111
- getCoverage(sampleResolution?: number): number;
112
- /** Find uncovered points for debugging gaps. */
113
- getUncoveredPoints(sampleResolution?: number): Array<{
114
- x: number;
115
- y: number;
116
- z: number;
117
- }>;
118
110
  /** Get color based on z layer for visualization. */
119
111
  private getColorForZLayer;
120
112
  /** Streaming visualization: board + obstacles + current placements. */
package/dist/index.js CHANGED
@@ -1117,50 +1117,6 @@ function rectsToMeshNodes(rects) {
1117
1117
  return out;
1118
1118
  }
1119
1119
 
1120
- // lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts
1121
- function calculateCoverage({ sampleResolution = 0.1 }, ctx) {
1122
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx;
1123
- let totalPoints = 0;
1124
- let coveredPoints = 0;
1125
- for (let z = 0; z < layerCount; z++) {
1126
- const obstacles = obstaclesByLayer[z] ?? [];
1127
- const placed = placedByLayer[z] ?? [];
1128
- const allRects = [...obstacles, ...placed];
1129
- for (let x = bounds.x; x <= bounds.x + bounds.width; x += sampleResolution) {
1130
- for (let y = bounds.y; y <= bounds.y + bounds.height; y += sampleResolution) {
1131
- totalPoints++;
1132
- const isCovered = allRects.some(
1133
- (r) => x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height
1134
- );
1135
- if (isCovered) coveredPoints++;
1136
- }
1137
- }
1138
- }
1139
- return totalPoints > 0 ? coveredPoints / totalPoints : 1;
1140
- }
1141
-
1142
- // lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts
1143
- function findUncoveredPoints({ sampleResolution = 0.05 }, ctx) {
1144
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx;
1145
- const uncovered = [];
1146
- for (let z = 0; z < layerCount; z++) {
1147
- const obstacles = obstaclesByLayer[z] ?? [];
1148
- const placed = placedByLayer[z] ?? [];
1149
- const allRects = [...obstacles, ...placed];
1150
- for (let x = bounds.x; x <= bounds.x + bounds.width; x += sampleResolution) {
1151
- for (let y = bounds.y; y <= bounds.y + bounds.height; y += sampleResolution) {
1152
- const isCovered = allRects.some(
1153
- (r) => x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height
1154
- );
1155
- if (!isCovered) {
1156
- uncovered.push({ x, y, z });
1157
- }
1158
- }
1159
- }
1160
- }
1161
- return uncovered;
1162
- }
1163
-
1164
1120
  // lib/solvers/RectDiffSolver.ts
1165
1121
  var RectDiffSolver = class extends BaseSolver {
1166
1122
  srj;
@@ -1209,30 +1165,6 @@ var RectDiffSolver = class extends BaseSolver {
1209
1165
  getOutput() {
1210
1166
  return { meshNodes: this._meshNodes };
1211
1167
  }
1212
- /** Get coverage percentage (0-1). */
1213
- getCoverage(sampleResolution = 0.05) {
1214
- return calculateCoverage(
1215
- { sampleResolution },
1216
- {
1217
- bounds: this.state.bounds,
1218
- layerCount: this.state.layerCount,
1219
- obstaclesByLayer: this.state.obstaclesByLayer,
1220
- placedByLayer: this.state.placedByLayer
1221
- }
1222
- );
1223
- }
1224
- /** Find uncovered points for debugging gaps. */
1225
- getUncoveredPoints(sampleResolution = 0.05) {
1226
- return findUncoveredPoints(
1227
- { sampleResolution },
1228
- {
1229
- bounds: this.state.bounds,
1230
- layerCount: this.state.layerCount,
1231
- obstaclesByLayer: this.state.obstaclesByLayer,
1232
- placedByLayer: this.state.placedByLayer
1233
- }
1234
- );
1235
- }
1236
1168
  /** Get color based on z layer for visualization. */
1237
1169
  getColorForZLayer(zLayers) {
1238
1170
  const minZ = Math.min(...zLayers);
@@ -14,11 +14,6 @@ import {
14
14
  } from "./rectdiff/engine"
15
15
  import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
16
16
  import { overlaps } from "./rectdiff/geometry"
17
- import type { GapFillOptions } from "./rectdiff/gapfill/types"
18
- import {
19
- findUncoveredPoints,
20
- calculateCoverage,
21
- } from "./rectdiff/gapfill/engine"
22
17
 
23
18
  /**
24
19
  * A streaming, one-step-per-iteration solver for capacity mesh generation.
@@ -82,34 +77,6 @@ export class RectDiffSolver extends BaseSolver {
82
77
  return { meshNodes: this._meshNodes }
83
78
  }
84
79
 
85
- /** Get coverage percentage (0-1). */
86
- getCoverage(sampleResolution: number = 0.05): number {
87
- return calculateCoverage(
88
- { sampleResolution },
89
- {
90
- bounds: this.state.bounds,
91
- layerCount: this.state.layerCount,
92
- obstaclesByLayer: this.state.obstaclesByLayer,
93
- placedByLayer: this.state.placedByLayer,
94
- },
95
- )
96
- }
97
-
98
- /** Find uncovered points for debugging gaps. */
99
- getUncoveredPoints(
100
- sampleResolution: number = 0.05,
101
- ): Array<{ x: number; y: number; z: number }> {
102
- return findUncoveredPoints(
103
- { sampleResolution },
104
- {
105
- bounds: this.state.bounds,
106
- layerCount: this.state.layerCount,
107
- obstaclesByLayer: this.state.obstaclesByLayer,
108
- placedByLayer: this.state.placedByLayer,
109
- },
110
- )
111
- }
112
-
113
80
  /** Get color based on z layer for visualization. */
114
81
  private getColorForZLayer(zLayers: number[]): {
115
82
  fill: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,28 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts
2
- import { rectsEqual } from "../../../../../utils/rectsEqual"
3
- import { rectsOverlap } from "../../../../../utils/rectsOverlap"
4
- import type { GapRegion } from "../types"
5
-
6
- export function deduplicateGaps(gaps: GapRegion[]): GapRegion[] {
7
- const result: GapRegion[] = []
8
-
9
- for (const gap of gaps) {
10
- // Check if we already have a gap at the same location with overlapping layers
11
- const existing = result.find(
12
- (g) =>
13
- rectsEqual(g.rect, gap.rect) ||
14
- (rectsOverlap(g.rect, gap.rect) &&
15
- gap.zLayers.some((z) => g.zLayers.includes(z))),
16
- )
17
-
18
- if (!existing) {
19
- result.push(gap)
20
- } else if (gap.zLayers.length > existing.zLayers.length) {
21
- // Replace with the one that has more layers
22
- const idx = result.indexOf(existing)
23
- result[idx] = gap
24
- }
25
- }
26
-
27
- return result
28
- }
@@ -1,83 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts
2
- import type { XYRect } from "../../types"
3
- import type { GapRegion, LayerContext } from "../types"
4
- import { EPS } from "../../geometry"
5
- import { findGapsOnLayer } from "./findGapsOnLayer"
6
- import { rectsOverlap } from "../../../../../utils/rectsOverlap"
7
- import { deduplicateGaps } from "./deduplicateGaps"
8
-
9
- /**
10
- * Find gaps across all layers and return GapRegions with z-layer info.
11
- */
12
- export function findAllGaps(
13
- {
14
- scanResolution,
15
- minWidth,
16
- minHeight,
17
- }: {
18
- scanResolution: number
19
- minWidth: number
20
- minHeight: number
21
- },
22
- ctx: LayerContext,
23
- ): GapRegion[] {
24
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx
25
-
26
- // Find gaps on each layer
27
- const gapsByLayer: XYRect[][] = []
28
- for (let z = 0; z < layerCount; z++) {
29
- const obstacles = obstaclesByLayer[z] ?? []
30
- const placed = placedByLayer[z] ?? []
31
- const gaps = findGapsOnLayer({ bounds, obstacles, placed, scanResolution })
32
- gapsByLayer.push(gaps)
33
- }
34
-
35
- // Convert to GapRegions with z-layer info
36
- const allGaps: GapRegion[] = []
37
-
38
- for (let z = 0; z < layerCount; z++) {
39
- for (const gap of gapsByLayer[z]!) {
40
- // Filter out gaps that are too small
41
- if (gap.width < minWidth - EPS || gap.height < minHeight - EPS) continue
42
-
43
- // Check if this gap exists on adjacent layers too
44
- const zLayers = [z]
45
-
46
- // Look up
47
- for (let zu = z + 1; zu < layerCount; zu++) {
48
- const hasOverlap = gapsByLayer[zu]!.some((g) => rectsOverlap(g, gap))
49
- if (hasOverlap) zLayers.push(zu)
50
- else break
51
- }
52
-
53
- // Look down (if z > 0 and not already counted)
54
- for (let zd = z - 1; zd >= 0; zd--) {
55
- const hasOverlap = gapsByLayer[zd]!.some((g) => rectsOverlap(g, gap))
56
- if (hasOverlap && !zLayers.includes(zd)) zLayers.unshift(zd)
57
- else break
58
- }
59
-
60
- allGaps.push({
61
- rect: gap,
62
- zLayers: zLayers.sort((a, b) => a - b),
63
- centerX: gap.x + gap.width / 2,
64
- centerY: gap.y + gap.height / 2,
65
- area: gap.width * gap.height,
66
- })
67
- }
68
- }
69
-
70
- // Deduplicate gaps that are essentially the same across layers
71
- const deduped = deduplicateGaps(allGaps)
72
-
73
- // Sort by priority: prefer larger gaps and multi-layer gaps
74
- deduped.sort((a, b) => {
75
- // Prefer multi-layer gaps
76
- const layerDiff = b.zLayers.length - a.zLayers.length
77
- if (layerDiff !== 0) return layerDiff
78
- // Then prefer larger area
79
- return b.area - a.area
80
- })
81
-
82
- return deduped
83
- }
@@ -1,100 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts
2
- import type { XYRect } from "../../types"
3
- import { EPS } from "../../geometry"
4
-
5
- import { mergeUncoveredCells } from "./mergeUncoveredCells"
6
-
7
- /**
8
- * Sweep-line algorithm to find maximal uncovered rectangles on a single layer.
9
- */
10
- export function findGapsOnLayer({
11
- bounds,
12
- obstacles,
13
- placed,
14
- scanResolution,
15
- }: {
16
- bounds: XYRect
17
- obstacles: XYRect[]
18
- placed: XYRect[]
19
- scanResolution: number
20
- }): XYRect[] {
21
- const blockers = [...obstacles, ...placed]
22
-
23
- // Collect all unique x-coordinates
24
- const xCoords = new Set<number>()
25
- xCoords.add(bounds.x)
26
- xCoords.add(bounds.x + bounds.width)
27
-
28
- for (const b of blockers) {
29
- if (b.x > bounds.x && b.x < bounds.x + bounds.width) {
30
- xCoords.add(b.x)
31
- }
32
- if (b.x + b.width > bounds.x && b.x + b.width < bounds.x + bounds.width) {
33
- xCoords.add(b.x + b.width)
34
- }
35
- }
36
-
37
- // Also add intermediate points based on scan resolution
38
- for (let x = bounds.x; x <= bounds.x + bounds.width; x += scanResolution) {
39
- xCoords.add(x)
40
- }
41
-
42
- const sortedX = Array.from(xCoords).sort((a, b) => a - b)
43
-
44
- // Similarly for y-coordinates
45
- const yCoords = new Set<number>()
46
- yCoords.add(bounds.y)
47
- yCoords.add(bounds.y + bounds.height)
48
-
49
- for (const b of blockers) {
50
- if (b.y > bounds.y && b.y < bounds.y + bounds.height) {
51
- yCoords.add(b.y)
52
- }
53
- if (
54
- b.y + b.height > bounds.y &&
55
- b.y + b.height < bounds.y + bounds.height
56
- ) {
57
- yCoords.add(b.y + b.height)
58
- }
59
- }
60
-
61
- for (let y = bounds.y; y <= bounds.y + bounds.height; y += scanResolution) {
62
- yCoords.add(y)
63
- }
64
-
65
- const sortedY = Array.from(yCoords).sort((a, b) => a - b)
66
-
67
- // Build a grid of cells and mark which are uncovered
68
- const uncoveredCells: Array<{ x: number; y: number; w: number; h: number }> =
69
- []
70
-
71
- for (let i = 0; i < sortedX.length - 1; i++) {
72
- for (let j = 0; j < sortedY.length - 1; j++) {
73
- const cellX = sortedX[i]!
74
- const cellY = sortedY[j]!
75
- const cellW = sortedX[i + 1]! - cellX
76
- const cellH = sortedY[j + 1]! - cellY
77
-
78
- if (cellW <= EPS || cellH <= EPS) continue
79
-
80
- // Check if this cell is covered by any blocker
81
- const cellCenterX = cellX + cellW / 2
82
- const cellCenterY = cellY + cellH / 2
83
-
84
- const isCovered = blockers.some(
85
- (b) =>
86
- cellCenterX >= b.x - EPS &&
87
- cellCenterX <= b.x + b.width + EPS &&
88
- cellCenterY >= b.y - EPS &&
89
- cellCenterY <= b.y + b.height + EPS,
90
- )
91
-
92
- if (!isCovered) {
93
- uncoveredCells.push({ x: cellX, y: cellY, w: cellW, h: cellH })
94
- }
95
- }
96
- }
97
-
98
- // Merge adjacent uncovered cells into maximal rectangles
99
- return mergeUncoveredCells(uncoveredCells)
100
- }
@@ -1,75 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts
2
- import type { XYRect } from "../../types"
3
- import { EPS } from "../../geometry"
4
-
5
- /**
6
- * Merge adjacent uncovered cells into larger rectangles using a greedy approach.
7
- */
8
- export function mergeUncoveredCells(
9
- cells: Array<{ x: number; y: number; w: number; h: number }>,
10
- ): XYRect[] {
11
- if (cells.length === 0) return []
12
-
13
- // Group cells by their left edge and width
14
- const byXW = new Map<string, typeof cells>()
15
- for (const c of cells) {
16
- const key = `${c.x.toFixed(9)}|${c.w.toFixed(9)}`
17
- const arr = byXW.get(key) ?? []
18
- arr.push(c)
19
- byXW.set(key, arr)
20
- }
21
-
22
- // Within each vertical strip, merge adjacent cells
23
- const verticalStrips: XYRect[] = []
24
- for (const stripCells of byXW.values()) {
25
- // Sort by y
26
- stripCells.sort((a, b) => a.y - b.y)
27
-
28
- let current: XYRect | null = null
29
- for (const c of stripCells) {
30
- if (!current) {
31
- current = { x: c.x, y: c.y, width: c.w, height: c.h }
32
- } else if (Math.abs(current.y + current.height - c.y) < EPS) {
33
- // Adjacent vertically, merge
34
- current.height += c.h
35
- } else {
36
- // Gap, save current and start new
37
- verticalStrips.push(current)
38
- current = { x: c.x, y: c.y, width: c.w, height: c.h }
39
- }
40
- }
41
- if (current) verticalStrips.push(current)
42
- }
43
-
44
- // Now try to merge horizontal strips with same y and height
45
- const byYH = new Map<string, XYRect[]>()
46
- for (const r of verticalStrips) {
47
- const key = `${r.y.toFixed(9)}|${r.height.toFixed(9)}`
48
- const arr = byYH.get(key) ?? []
49
- arr.push(r)
50
- byYH.set(key, arr)
51
- }
52
-
53
- const merged: XYRect[] = []
54
- for (const rowRects of byYH.values()) {
55
- // Sort by x
56
- rowRects.sort((a, b) => a.x - b.x)
57
-
58
- let current: XYRect | null = null
59
- for (const r of rowRects) {
60
- if (!current) {
61
- current = { ...r }
62
- } else if (Math.abs(current.x + current.width - r.x) < EPS) {
63
- // Adjacent horizontally, merge
64
- current.width += r.width
65
- } else {
66
- // Gap, save current and start new
67
- merged.push(current)
68
- current = { ...r }
69
- }
70
- }
71
- if (current) merged.push(current)
72
- }
73
-
74
- return merged
75
- }
@@ -1,3 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection.ts
2
- export * from "./detection/findAllGaps"
3
- // findGapsOnLayer is not exported as it's only used by findAllGaps
@@ -1,27 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/addPlacement.ts
2
- import type { Placed3D, XYRect } from "../../types"
3
- import type { GapFillState } from "../types"
4
-
5
- /**
6
- * Add a new placement to the state.
7
- */
8
- export function addPlacement(
9
- state: GapFillState,
10
- {
11
- rect,
12
- zLayers,
13
- }: {
14
- rect: XYRect
15
- zLayers: number[]
16
- },
17
- ): void {
18
- const placed: Placed3D = { rect, zLayers: [...zLayers] }
19
- state.placed.push(placed)
20
-
21
- for (const z of zLayers) {
22
- if (!state.placedByLayer[z]) {
23
- state.placedByLayer[z] = []
24
- }
25
- state.placedByLayer[z]!.push(rect)
26
- }
27
- }
@@ -1,44 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts
2
- import type { LayerContext } from "../types"
3
-
4
- /**
5
- * Calculate coverage percentage (0-1).
6
- */
7
- export function calculateCoverage(
8
- { sampleResolution = 0.1 }: { sampleResolution?: number },
9
- ctx: LayerContext,
10
- ): number {
11
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx
12
-
13
- let totalPoints = 0
14
- let coveredPoints = 0
15
-
16
- for (let z = 0; z < layerCount; z++) {
17
- const obstacles = obstaclesByLayer[z] ?? []
18
- const placed = placedByLayer[z] ?? []
19
- const allRects = [...obstacles, ...placed]
20
-
21
- for (
22
- let x = bounds.x;
23
- x <= bounds.x + bounds.width;
24
- x += sampleResolution
25
- ) {
26
- for (
27
- let y = bounds.y;
28
- y <= bounds.y + bounds.height;
29
- y += sampleResolution
30
- ) {
31
- totalPoints++
32
-
33
- const isCovered = allRects.some(
34
- (r) =>
35
- x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height,
36
- )
37
-
38
- if (isCovered) coveredPoints++
39
- }
40
- }
41
- }
42
-
43
- return totalPoints > 0 ? coveredPoints / totalPoints : 1
44
- }
@@ -1,43 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts
2
- import type { LayerContext } from "../types"
3
-
4
- /**
5
- * Find uncovered points for debugging gaps.
6
- */
7
- export function findUncoveredPoints(
8
- { sampleResolution = 0.05 }: { sampleResolution?: number },
9
- ctx: LayerContext,
10
- ): Array<{ x: number; y: number; z: number }> {
11
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx
12
-
13
- const uncovered: Array<{ x: number; y: number; z: number }> = []
14
-
15
- for (let z = 0; z < layerCount; z++) {
16
- const obstacles = obstaclesByLayer[z] ?? []
17
- const placed = placedByLayer[z] ?? []
18
- const allRects = [...obstacles, ...placed]
19
-
20
- for (
21
- let x = bounds.x;
22
- x <= bounds.x + bounds.width;
23
- x += sampleResolution
24
- ) {
25
- for (
26
- let y = bounds.y;
27
- y <= bounds.y + bounds.height;
28
- y += sampleResolution
29
- ) {
30
- const isCovered = allRects.some(
31
- (r) =>
32
- x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height,
33
- )
34
-
35
- if (!isCovered) {
36
- uncovered.push({ x, y, z })
37
- }
38
- }
39
- }
40
- }
41
-
42
- return uncovered
43
- }
@@ -1,42 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts
2
- import type { GapFillState } from "../types"
3
-
4
- /**
5
- * Get progress as a number between 0 and 1.
6
- * Accounts for four-stage processing (scan → select → expand → place for each gap).
7
- */
8
- export function getGapFillProgress(state: GapFillState): number {
9
- if (state.done) return 1
10
-
11
- const iterationProgress = state.iteration / state.options.maxIterations
12
- const gapProgress =
13
- state.gapsFound.length > 0 ? state.gapIndex / state.gapsFound.length : 0
14
-
15
- // Add sub-progress within current gap based on stage
16
- let stageProgress = 0
17
- switch (state.stage) {
18
- case "scan":
19
- stageProgress = 0
20
- break
21
- case "select":
22
- stageProgress = 0.25
23
- break
24
- case "expand":
25
- stageProgress = 0.5
26
- break
27
- case "place":
28
- stageProgress = 0.75
29
- break
30
- }
31
-
32
- const gapStageProgress =
33
- state.gapsFound.length > 0
34
- ? stageProgress / (state.gapsFound.length * 4) // 4 stages per gap
35
- : 0
36
-
37
- return Math.min(
38
- 0.999,
39
- iterationProgress +
40
- (gapProgress + gapStageProgress) / state.options.maxIterations,
41
- )
42
- }
@@ -1,57 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts
2
- import type { Placed3D } from "../../types"
3
- import type { GapFillState, GapFillOptions, LayerContext } from "../types"
4
-
5
- const DEFAULT_OPTIONS: GapFillOptions = {
6
- minWidth: 0.1,
7
- minHeight: 0.1,
8
- maxIterations: 10,
9
- targetCoverage: 0.999,
10
- scanResolution: 0.5,
11
- }
12
-
13
- /**
14
- * Initialize the gap fill state from existing rectdiff state.
15
- */
16
- export function initGapFillState(
17
- {
18
- placed,
19
- options,
20
- }: {
21
- placed: Placed3D[]
22
- options?: Partial<GapFillOptions>
23
- },
24
- ctx: LayerContext,
25
- ): GapFillState {
26
- const opts = { ...DEFAULT_OPTIONS, ...options }
27
-
28
- // Deep copy placed arrays to avoid mutation issues
29
- const placedCopy = placed.map((p) => ({
30
- rect: { ...p.rect },
31
- zLayers: [...p.zLayers],
32
- }))
33
-
34
- const placedByLayerCopy = ctx.placedByLayer.map((layer) =>
35
- layer.map((r) => ({ ...r })),
36
- )
37
-
38
- return {
39
- bounds: { ...ctx.bounds },
40
- layerCount: ctx.layerCount,
41
- obstaclesByLayer: ctx.obstaclesByLayer,
42
- placed: placedCopy,
43
- placedByLayer: placedByLayerCopy,
44
- options: opts,
45
- iteration: 0,
46
- gapsFound: [],
47
- gapIndex: 0,
48
- done: false,
49
- initialGapCount: 0,
50
- filledCount: 0,
51
- // Four-stage visualization state
52
- stage: "scan",
53
- currentGap: null,
54
- currentSeed: null,
55
- expandedRect: null,
56
- }
57
- }
@@ -1,128 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts
2
- import type { GapFillState } from "../types"
3
- import { findAllGaps } from "../detection"
4
- import { tryExpandGap } from "./tryExpandGap"
5
- import { addPlacement } from "./addPlacement"
6
-
7
- /**
8
- * Perform one step of gap filling with four-stage visualization.
9
- * Stages: scan → select → expand → place
10
- * Returns true if still working, false if done.
11
- */
12
- export function stepGapFill(state: GapFillState): boolean {
13
- if (state.done) return false
14
-
15
- switch (state.stage) {
16
- case "scan": {
17
- // Stage 1: Gap detection/scanning
18
-
19
- // Check if we need to find new gaps
20
- if (
21
- state.gapsFound.length === 0 ||
22
- state.gapIndex >= state.gapsFound.length
23
- ) {
24
- // Check if we've hit max iterations
25
- if (state.iteration >= state.options.maxIterations) {
26
- state.done = true
27
- return false
28
- }
29
-
30
- // Find new gaps
31
- state.gapsFound = findAllGaps(
32
- {
33
- scanResolution: state.options.scanResolution,
34
- minWidth: state.options.minWidth,
35
- minHeight: state.options.minHeight,
36
- },
37
- {
38
- bounds: state.bounds,
39
- layerCount: state.layerCount,
40
- obstaclesByLayer: state.obstaclesByLayer,
41
- placedByLayer: state.placedByLayer,
42
- },
43
- )
44
-
45
- if (state.iteration === 0) {
46
- state.initialGapCount = state.gapsFound.length
47
- }
48
-
49
- state.gapIndex = 0
50
- state.iteration++
51
-
52
- // If no gaps found, we're done
53
- if (state.gapsFound.length === 0) {
54
- state.done = true
55
- return false
56
- }
57
- }
58
-
59
- // Move to select stage
60
- state.stage = "select"
61
- return true
62
- }
63
-
64
- case "select": {
65
- // Stage 2: Show the gap being targeted
66
- if (state.gapIndex >= state.gapsFound.length) {
67
- // No more gaps in this iteration, go back to scan
68
- state.stage = "scan"
69
- return true
70
- }
71
-
72
- state.currentGap = state.gapsFound[state.gapIndex]!
73
- state.currentSeed = {
74
- x: state.currentGap.centerX,
75
- y: state.currentGap.centerY,
76
- }
77
- state.expandedRect = null
78
-
79
- // Move to expand stage
80
- state.stage = "expand"
81
- return true
82
- }
83
-
84
- case "expand": {
85
- // Stage 3: Show expansion attempt
86
- if (!state.currentGap) {
87
- // Shouldn't happen, but handle gracefully
88
- state.stage = "select"
89
- return true
90
- }
91
-
92
- // Try to expand from the current seed
93
- const expandedRect = tryExpandGap(state, {
94
- gap: state.currentGap,
95
- seed: state.currentSeed!,
96
- })
97
- state.expandedRect = expandedRect
98
-
99
- // Move to place stage
100
- state.stage = "place"
101
- return true
102
- }
103
-
104
- case "place": {
105
- // Stage 4: Show the placed result
106
- if (state.expandedRect && state.currentGap) {
107
- // Actually place the rectangle
108
- addPlacement(state, {
109
- rect: state.expandedRect,
110
- zLayers: state.currentGap.zLayers,
111
- })
112
- state.filledCount++
113
- }
114
-
115
- // Move to next gap and reset to select stage
116
- state.gapIndex++
117
- state.currentGap = null
118
- state.currentSeed = null
119
- state.expandedRect = null
120
- state.stage = "select"
121
- return true
122
- }
123
-
124
- default:
125
- state.stage = "scan"
126
- return true
127
- }
128
- }
@@ -1,78 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts
2
- import type { XYRect } from "../../types"
3
- import type { GapFillState, GapRegion } from "../types"
4
- import { expandRectFromSeed } from "../../geometry"
5
-
6
- /**
7
- * Try to expand a rectangle from a seed point within the gap.
8
- * Returns the expanded rectangle or null if expansion fails.
9
- */
10
- export function tryExpandGap(
11
- state: GapFillState,
12
- {
13
- gap,
14
- seed,
15
- }: {
16
- gap: GapRegion
17
- seed: { x: number; y: number }
18
- },
19
- ): XYRect | null {
20
- // Build blockers for the gap's z-layers
21
- const blockers: XYRect[] = []
22
- for (const z of gap.zLayers) {
23
- blockers.push(...(state.obstaclesByLayer[z] ?? []))
24
- blockers.push(...(state.placedByLayer[z] ?? []))
25
- }
26
-
27
- // Try to expand from the seed point
28
- const rect = expandRectFromSeed({
29
- startX: seed.x,
30
- startY: seed.y,
31
- gridSize: Math.min(gap.rect.width, gap.rect.height),
32
- bounds: state.bounds,
33
- blockers,
34
- initialCellRatio: 0,
35
- maxAspectRatio: null,
36
- minReq: { width: state.options.minWidth, height: state.options.minHeight },
37
- })
38
-
39
- if (!rect) {
40
- // Try additional seed points within the gap
41
- const seeds = [
42
- { x: gap.rect.x + state.options.minWidth / 2, y: gap.centerY },
43
- {
44
- x: gap.rect.x + gap.rect.width - state.options.minWidth / 2,
45
- y: gap.centerY,
46
- },
47
- { x: gap.centerX, y: gap.rect.y + state.options.minHeight / 2 },
48
- {
49
- x: gap.centerX,
50
- y: gap.rect.y + gap.rect.height - state.options.minHeight / 2,
51
- },
52
- ]
53
-
54
- for (const altSeed of seeds) {
55
- const altRect = expandRectFromSeed({
56
- startX: altSeed.x,
57
- startY: altSeed.y,
58
- gridSize: Math.min(gap.rect.width, gap.rect.height),
59
- bounds: state.bounds,
60
- blockers,
61
- initialCellRatio: 0,
62
- maxAspectRatio: null,
63
- minReq: {
64
- width: state.options.minWidth,
65
- height: state.options.minHeight,
66
- },
67
- })
68
-
69
- if (altRect) {
70
- return altRect
71
- }
72
- }
73
-
74
- return null
75
- }
76
-
77
- return rect
78
- }
@@ -1,7 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine.ts
2
- export * from "./engine/calculateCoverage"
3
- export * from "./engine/findUncoveredPoints"
4
- export * from "./engine/getGapFillProgress"
5
- export * from "./engine/initGapFillState"
6
-
7
- export * from "./engine/stepGapFill"
@@ -1,60 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/types.ts
2
- import type { XYRect, Placed3D } from "../types"
3
-
4
- export interface GapFillOptions {
5
- /** Minimum width for gap-fill rectangles (can be smaller than main solver) */
6
- minWidth: number
7
- /** Minimum height for gap-fill rectangles */
8
- minHeight: number
9
- /** Maximum iterations to prevent infinite loops */
10
- maxIterations: number
11
- /** Target coverage percentage (0-1) to stop early */
12
- targetCoverage: number
13
- /** Grid resolution for gap detection */
14
- scanResolution: number
15
- }
16
-
17
- export interface GapRegion {
18
- /** Bounding box of the gap */
19
- rect: XYRect
20
- /** Z-layers where this gap exists */
21
- zLayers: number[]
22
- /** Center point for seeding */
23
- centerX: number
24
- centerY: number
25
- /** Approximate area of the gap */
26
- area: number
27
- }
28
-
29
- export interface GapFillState {
30
- bounds: XYRect
31
- layerCount: number
32
- obstaclesByLayer: XYRect[][]
33
- placed: Placed3D[]
34
- placedByLayer: XYRect[][]
35
- options: GapFillOptions
36
-
37
- // Progress tracking
38
- iteration: number
39
- gapsFound: GapRegion[]
40
- gapIndex: number
41
- done: boolean
42
-
43
- // Stats
44
- initialGapCount: number
45
- filledCount: number
46
-
47
- // Four-stage visualization state
48
- stage: "scan" | "select" | "expand" | "place"
49
- currentGap: GapRegion | null
50
- currentSeed: { x: number; y: number } | null
51
- expandedRect: XYRect | null
52
- }
53
-
54
- /** Context for layer-based operations shared across gap fill functions */
55
- export interface LayerContext {
56
- bounds: XYRect
57
- layerCount: number
58
- obstaclesByLayer: XYRect[][]
59
- placedByLayer: XYRect[][]
60
- }
@@ -1,253 +0,0 @@
1
- // lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts
2
- import { BaseSolver } from "@tscircuit/solver-utils"
3
- import type { GraphicsObject } from "graphics-debug"
4
- import type { XYRect, Placed3D } from "../types"
5
- import type {
6
- GapFillState,
7
- GapFillOptions,
8
- LayerContext,
9
- } from "../gapfill/types"
10
- import {
11
- initGapFillState,
12
- stepGapFill,
13
- getGapFillProgress,
14
- } from "../gapfill/engine"
15
-
16
- /**
17
- * A sub-solver that fills empty spaces (gaps) left by the main grid-based
18
- * placement algorithm.
19
- *
20
- * The preceding grid-based placement is fast but can leave irregular un-placed
21
- * areas. This solver maximizes board coverage by finding and filling these
22
- * gaps, which is critical for producing a high-quality capacity mesh.
23
- *
24
- * The core of the algorithm is its gap-detection phase. It works by first
25
- * collecting all unique x and y-coordinates from the edges of existing
26
- * obstacles and placed rectangles. This set of coordinates is supplemented by a
27
- * uniform grid based on the `scanResolution` parameter. Together, these form a
28
- * non-uniform grid of cells. The solver then tests the center of each cell for
29
- * coverage. Contiguous uncovered cells are merged into larger, maximal
30
- * rectangles, which become the candidate gaps to be filled.
31
- *
32
- * Once a prioritized list of gaps is generated (favoring larger, multi-layer
33
- * gaps), the solver iteratively attempts to fill each one by expanding a new
34
- * rectangle from a seed point until it collides with an existing boundary.
35
- *
36
- * The time complexity is dominated by the gap detection, which is approximately
37
- * O((N+1/R)^2 * B), where N is the number of objects, R is the scan
38
- * resolution, and B is the number of blockers. The algorithm's performance is
39
- * therefore highly dependent on the `scanResolution`. It is a heuristic
40
- * designed to be "fast enough" by avoiding a brute-force search, instead
41
- * relying on this grid-based cell checking to find significant gaps.
42
- */
43
- export class GapFillSubSolver extends BaseSolver {
44
- private state: GapFillState
45
- private layerCtx: LayerContext
46
-
47
- constructor(params: {
48
- placed: Placed3D[]
49
- options?: Partial<GapFillOptions>
50
- layerCtx: LayerContext
51
- }) {
52
- super()
53
- this.layerCtx = params.layerCtx
54
- this.state = initGapFillState(
55
- {
56
- placed: params.placed,
57
- options: params.options,
58
- },
59
- params.layerCtx,
60
- )
61
- }
62
-
63
- /**
64
- * Execute one step of the gap fill algorithm.
65
- * Each gap goes through four stages: scan for gaps, select a target gap,
66
- * expand a rectangle from seed point, then place the final result.
67
- */
68
- override _step() {
69
- const stillWorking = stepGapFill(this.state)
70
- if (!stillWorking) {
71
- this.solved = true
72
- }
73
- }
74
-
75
- /**
76
- * Calculate progress as a value between 0 and 1.
77
- * Accounts for iterations, gaps processed, and current stage within each gap.
78
- */
79
- computeProgress(): number {
80
- return getGapFillProgress(this.state)
81
- }
82
-
83
- /**
84
- * Get all placed rectangles including original ones plus newly created gap-fill rectangles.
85
- */
86
- getPlaced(): Placed3D[] {
87
- return this.state.placed
88
- }
89
-
90
- /**
91
- * Get placed rectangles organized by Z-layer for efficient layer-based operations.
92
- */
93
- getPlacedByLayer(): XYRect[][] {
94
- return this.state.placedByLayer
95
- }
96
-
97
- override getOutput() {
98
- return {
99
- placed: this.state.placed,
100
- placedByLayer: this.state.placedByLayer,
101
- filledCount: this.state.filledCount,
102
- }
103
- }
104
-
105
- /** Zen visualization: show four-stage gap filling process. */
106
- override visualize(): GraphicsObject {
107
- const rects: NonNullable<GraphicsObject["rects"]> = []
108
- const points: NonNullable<GraphicsObject["points"]> = []
109
-
110
- // Board bounds (subtle)
111
- rects.push({
112
- center: {
113
- x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2,
114
- y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2,
115
- },
116
- width: this.layerCtx.bounds.width,
117
- height: this.layerCtx.bounds.height,
118
- fill: "none",
119
- stroke: "#e5e7eb",
120
- label: "",
121
- })
122
-
123
- switch (this.state.stage) {
124
- case "scan": {
125
- // Stage 1: Show scanning/detection phase with light blue overlay
126
- rects.push({
127
- center: {
128
- x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2,
129
- y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2,
130
- },
131
- width: this.layerCtx.bounds.width,
132
- height: this.layerCtx.bounds.height,
133
- fill: "#dbeafe",
134
- stroke: "#3b82f6",
135
- label: "scanning",
136
- })
137
- break
138
- }
139
-
140
- case "select": {
141
- // Stage 2: Show the gap being targeted (red outline)
142
- if (this.state.currentGap) {
143
- rects.push({
144
- center: {
145
- x:
146
- this.state.currentGap.rect.x +
147
- this.state.currentGap.rect.width / 2,
148
- y:
149
- this.state.currentGap.rect.y +
150
- this.state.currentGap.rect.height / 2,
151
- },
152
- width: this.state.currentGap.rect.width,
153
- height: this.state.currentGap.rect.height,
154
- fill: "#fecaca",
155
- stroke: "#ef4444",
156
- label: "target gap",
157
- })
158
-
159
- // Show the seed point
160
- if (this.state.currentSeed) {
161
- points.push({
162
- x: this.state.currentSeed.x,
163
- y: this.state.currentSeed.y,
164
- color: "#dc2626",
165
- label: "seed",
166
- })
167
- }
168
- }
169
- break
170
- }
171
-
172
- case "expand": {
173
- // Stage 3: Show expansion attempt (yellow growing rectangle + seed)
174
- if (this.state.currentGap) {
175
- // Show gap outline (faded)
176
- rects.push({
177
- center: {
178
- x:
179
- this.state.currentGap.rect.x +
180
- this.state.currentGap.rect.width / 2,
181
- y:
182
- this.state.currentGap.rect.y +
183
- this.state.currentGap.rect.height / 2,
184
- },
185
- width: this.state.currentGap.rect.width,
186
- height: this.state.currentGap.rect.height,
187
- fill: "none",
188
- stroke: "#f87171",
189
- label: "",
190
- })
191
- }
192
-
193
- if (this.state.currentSeed) {
194
- // Show seed point
195
- points.push({
196
- x: this.state.currentSeed.x,
197
- y: this.state.currentSeed.y,
198
- color: "#f59e0b",
199
- label: "expanding",
200
- })
201
- }
202
-
203
- if (this.state.expandedRect) {
204
- // Show expanded rectangle
205
- rects.push({
206
- center: {
207
- x: this.state.expandedRect.x + this.state.expandedRect.width / 2,
208
- y: this.state.expandedRect.y + this.state.expandedRect.height / 2,
209
- },
210
- width: this.state.expandedRect.width,
211
- height: this.state.expandedRect.height,
212
- fill: "#fef3c7",
213
- stroke: "#f59e0b",
214
- label: "expanding",
215
- })
216
- }
217
- break
218
- }
219
-
220
- case "place": {
221
- // Stage 4: Show final placed rectangle (green)
222
- if (this.state.expandedRect) {
223
- rects.push({
224
- center: {
225
- x: this.state.expandedRect.x + this.state.expandedRect.width / 2,
226
- y: this.state.expandedRect.y + this.state.expandedRect.height / 2,
227
- },
228
- width: this.state.expandedRect.width,
229
- height: this.state.expandedRect.height,
230
- fill: "#bbf7d0",
231
- stroke: "#22c55e",
232
- label: "placed",
233
- })
234
- }
235
- break
236
- }
237
- }
238
-
239
- const stageNames = {
240
- scan: "scanning",
241
- select: "selecting",
242
- expand: "expanding",
243
- place: "placing",
244
- }
245
-
246
- return {
247
- title: `GapFill (${stageNames[this.state.stage]}): ${this.state.filledCount} filled`,
248
- coordinateSystem: "cartesian",
249
- rects,
250
- points,
251
- }
252
- }
253
- }