@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 +0 -8
- package/dist/index.js +0 -68
- package/lib/solvers/RectDiffSolver.ts +0 -33
- package/package.json +1 -1
- package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +0 -28
- package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +0 -83
- package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +0 -100
- package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +0 -75
- package/lib/solvers/rectdiff/gapfill/detection.ts +0 -3
- package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +0 -27
- package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +0 -44
- package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +0 -43
- package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +0 -42
- package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +0 -57
- package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +0 -128
- package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +0 -78
- package/lib/solvers/rectdiff/gapfill/engine.ts +0 -7
- package/lib/solvers/rectdiff/gapfill/types.ts +0 -60
- package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +0 -253
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,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,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,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
|
-
}
|