Skip to content

Commit 58de2db

Browse files
authored
Add Sub-solvers to fix the gap problem (#11)
* feat: implement GapFillSubSolver
1 parent 74a80ee commit 58de2db

26 files changed

+1459
-199
lines changed

lib/solvers/RectDiffSolver.ts

Lines changed: 125 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,91 @@ import {
1313
computeProgress,
1414
} from "./rectdiff/engine"
1515
import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
16+
import type { GapFillOptions } from "./rectdiff/gapfill/types"
17+
import {
18+
findUncoveredPoints,
19+
calculateCoverage,
20+
} from "./rectdiff/gapfill/engine"
21+
import { GapFillSubSolver } from "./rectdiff/subsolvers/GapFillSubSolver"
1622

17-
// A streaming, one-step-per-iteration solver.
18-
// Tests that call `solver.solve()` still work because BaseSolver.solve()
19-
// loops until this.solved flips true.
20-
23+
/**
24+
* A streaming, one-step-per-iteration solver for capacity mesh generation.
25+
*/
2126
export class RectDiffSolver extends BaseSolver {
2227
private srj: SimpleRouteJson
23-
private mode: "grid" | "exact"
2428
private gridOptions: Partial<GridFill3DOptions>
29+
private gapFillOptions: Partial<GapFillOptions>
2530
private state!: RectDiffState
2631
private _meshNodes: CapacityMeshNode[] = []
2732

33+
/** Active subsolver for GAP_FILL phases. */
34+
declare activeSubSolver: GapFillSubSolver | null
35+
2836
constructor(opts: {
2937
simpleRouteJson: SimpleRouteJson
30-
mode?: "grid" | "exact"
3138
gridOptions?: Partial<GridFill3DOptions>
39+
gapFillOptions?: Partial<GapFillOptions>
3240
}) {
3341
super()
3442
this.srj = opts.simpleRouteJson
35-
this.mode = opts.mode ?? "grid"
3643
this.gridOptions = opts.gridOptions ?? {}
44+
this.gapFillOptions = opts.gapFillOptions ?? {}
45+
this.activeSubSolver = null
3746
}
3847

3948
override _setup() {
40-
// For now "exact" mode falls back to grid; keep switch if you add exact later.
4149
this.state = initState(this.srj, this.gridOptions)
4250
this.stats = {
4351
phase: this.state.phase,
4452
gridIndex: this.state.gridIndex,
4553
}
4654
}
4755

48-
/** IMPORTANT: exactly ONE small step per call */
56+
/** Exactly ONE small step per call. */
4957
override _step() {
5058
if (this.state.phase === "GRID") {
5159
stepGrid(this.state)
5260
} else if (this.state.phase === "EXPANSION") {
5361
stepExpansion(this.state)
62+
} else if (this.state.phase === "GAP_FILL") {
63+
// Initialize gap fill subsolver if needed
64+
if (
65+
!this.activeSubSolver ||
66+
!(this.activeSubSolver instanceof GapFillSubSolver)
67+
) {
68+
const minTrace = this.srj.minTraceWidth || 0.15
69+
const minGapSize = Math.max(0.01, minTrace / 10)
70+
const boundsSize = Math.min(
71+
this.state.bounds.width,
72+
this.state.bounds.height,
73+
)
74+
this.activeSubSolver = new GapFillSubSolver({
75+
placed: this.state.placed,
76+
options: {
77+
minWidth: minGapSize,
78+
minHeight: minGapSize,
79+
scanResolution: Math.max(0.05, boundsSize / 100),
80+
...this.gapFillOptions,
81+
},
82+
layerCtx: {
83+
bounds: this.state.bounds,
84+
layerCount: this.state.layerCount,
85+
obstaclesByLayer: this.state.obstaclesByLayer,
86+
placedByLayer: this.state.placedByLayer,
87+
},
88+
})
89+
}
90+
91+
this.activeSubSolver.step()
92+
93+
if (this.activeSubSolver.solved) {
94+
// Transfer results back to main state
95+
const output = this.activeSubSolver.getOutput()
96+
this.state.placed = output.placed
97+
this.state.placedByLayer = output.placedByLayer
98+
this.activeSubSolver = null
99+
this.state.phase = "DONE"
100+
}
54101
} else if (this.state.phase === "DONE") {
55102
// Finalize once
56103
if (!this.solved) {
@@ -65,47 +112,101 @@ export class RectDiffSolver extends BaseSolver {
65112
this.stats.phase = this.state.phase
66113
this.stats.gridIndex = this.state.gridIndex
67114
this.stats.placed = this.state.placed.length
115+
if (this.activeSubSolver instanceof GapFillSubSolver) {
116+
const output = this.activeSubSolver.getOutput()
117+
this.stats.gapsFilled = output.filledCount
118+
}
68119
}
69120

70-
// Let BaseSolver update this.progress automatically if present.
121+
/** Compute solver progress (0 to 1). */
71122
computeProgress(): number {
72-
return computeProgress(this.state)
123+
if (this.solved || this.state.phase === "DONE") {
124+
return 1
125+
}
126+
if (
127+
this.state.phase === "GAP_FILL" &&
128+
this.activeSubSolver instanceof GapFillSubSolver
129+
) {
130+
return 0.85 + 0.1 * this.activeSubSolver.computeProgress()
131+
}
132+
return computeProgress(this.state) * 0.85
73133
}
74134

75135
override getOutput(): { meshNodes: CapacityMeshNode[] } {
76136
return { meshNodes: this._meshNodes }
77137
}
78138

79-
// Helper to get color based on z layer
139+
/** Get coverage percentage (0-1). */
140+
getCoverage(sampleResolution: number = 0.05): number {
141+
return calculateCoverage(
142+
{ sampleResolution },
143+
{
144+
bounds: this.state.bounds,
145+
layerCount: this.state.layerCount,
146+
obstaclesByLayer: this.state.obstaclesByLayer,
147+
placedByLayer: this.state.placedByLayer,
148+
},
149+
)
150+
}
151+
152+
/** Find uncovered points for debugging gaps. */
153+
getUncoveredPoints(
154+
sampleResolution: number = 0.05,
155+
): Array<{ x: number; y: number; z: number }> {
156+
return findUncoveredPoints(
157+
{ sampleResolution },
158+
{
159+
bounds: this.state.bounds,
160+
layerCount: this.state.layerCount,
161+
obstaclesByLayer: this.state.obstaclesByLayer,
162+
placedByLayer: this.state.placedByLayer,
163+
},
164+
)
165+
}
166+
167+
/** Get color based on z layer for visualization. */
80168
private getColorForZLayer(zLayers: number[]): {
81169
fill: string
82170
stroke: string
83171
} {
84172
const minZ = Math.min(...zLayers)
85173
const colors = [
86-
{ fill: "#dbeafe", stroke: "#3b82f6" }, // blue (z=0)
87-
{ fill: "#fef3c7", stroke: "#f59e0b" }, // amber (z=1)
88-
{ fill: "#d1fae5", stroke: "#10b981" }, // green (z=2)
89-
{ fill: "#e9d5ff", stroke: "#a855f7" }, // purple (z=3)
90-
{ fill: "#fed7aa", stroke: "#f97316" }, // orange (z=4)
91-
{ fill: "#fecaca", stroke: "#ef4444" }, // red (z=5)
174+
{ fill: "#dbeafe", stroke: "#3b82f6" },
175+
{ fill: "#fef3c7", stroke: "#f59e0b" },
176+
{ fill: "#d1fae5", stroke: "#10b981" },
177+
{ fill: "#e9d5ff", stroke: "#a855f7" },
178+
{ fill: "#fed7aa", stroke: "#f97316" },
179+
{ fill: "#fecaca", stroke: "#ef4444" },
92180
]
93181
return colors[minZ % colors.length]!
94182
}
95183

96-
// Streaming visualization: board + obstacles + current placements.
184+
/** Streaming visualization: board + obstacles + current placements. */
97185
override visualize(): GraphicsObject {
186+
// If a subsolver is active, delegate to its visualization
187+
if (this.activeSubSolver) {
188+
return this.activeSubSolver.visualize()
189+
}
190+
98191
const rects: NonNullable<GraphicsObject["rects"]> = []
99192
const points: NonNullable<GraphicsObject["points"]> = []
100193

194+
// Board bounds - use srj bounds which is always available
195+
const boardBounds = {
196+
minX: this.srj.bounds.minX,
197+
maxX: this.srj.bounds.maxX,
198+
minY: this.srj.bounds.minY,
199+
maxY: this.srj.bounds.maxY,
200+
}
201+
101202
// board
102203
rects.push({
103204
center: {
104-
x: (this.srj.bounds.minX + this.srj.bounds.maxX) / 2,
105-
y: (this.srj.bounds.minY + this.srj.bounds.maxY) / 2,
205+
x: (boardBounds.minX + boardBounds.maxX) / 2,
206+
y: (boardBounds.minY + boardBounds.maxY) / 2,
106207
},
107-
width: this.srj.bounds.maxX - this.srj.bounds.minX,
108-
height: this.srj.bounds.maxY - this.srj.bounds.minY,
208+
width: boardBounds.maxX - boardBounds.minX,
209+
height: boardBounds.maxY - boardBounds.minY,
109210
fill: "none",
110211
stroke: "#111827",
111212
label: "board",
@@ -158,7 +259,7 @@ export class RectDiffSolver extends BaseSolver {
158259
}
159260

160261
return {
161-
title: "RectDiff (incremental)",
262+
title: `RectDiff (${this.state?.phase ?? "init"})`,
162263
coordinateSystem: "cartesian",
163264
rects,
164265
points,

0 commit comments

Comments
 (0)