@@ -13,44 +13,91 @@ import {
1313 computeProgress ,
1414} from "./rectdiff/engine"
1515import { 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+ */
2126export 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