Skip to content

Commit 04437f1

Browse files
committed
WIP
1 parent 972e706 commit 04437f1

20 files changed

Lines changed: 1678 additions & 20 deletions

lib/solvers/RectDiffSolver.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
findUncoveredPoints,
2020
calculateCoverage,
2121
} from "./rectdiff/gapfill/engine"
22+
import { EdgeExpansionGapFillSubSolver } from "./rectdiff/subsolvers/EdgeExpansionGapFillSubSolver"
2223

2324
/**
2425
* A streaming, one-step-per-iteration solver for capacity mesh generation.
@@ -28,6 +29,7 @@ export class RectDiffSolver extends BaseSolver {
2829
private gridOptions: Partial<GridFill3DOptions>
2930
private state!: RectDiffState
3031
private _meshNodes: CapacityMeshNode[] = []
32+
private gapFillSubSolver?: EdgeExpansionGapFillSubSolver
3133

3234
constructor(opts: {
3335
simpleRouteJson: SimpleRouteJson
@@ -53,7 +55,44 @@ export class RectDiffSolver extends BaseSolver {
5355
} else if (this.state.phase === "EXPANSION") {
5456
stepExpansion(this.state)
5557
} else if (this.state.phase === "GAP_FILL") {
56-
this.state.phase = "DONE"
58+
// Initialize gap fill subsolver on first entry
59+
if (!this.gapFillSubSolver) {
60+
this.gapFillSubSolver = new EdgeExpansionGapFillSubSolver({
61+
bounds: this.state.bounds,
62+
layerCount: this.state.layerCount,
63+
obstacles: this.state.obstaclesByLayer,
64+
existingPlaced: this.state.placed,
65+
existingPlacedByLayer: this.state.placedByLayer,
66+
options: {
67+
minSingle: this.state.options.minSingle,
68+
minMulti: this.state.options.minMulti,
69+
maxAspectRatio: this.state.options.maxAspectRatio,
70+
maxMultiLayerSpan: this.state.options.maxMultiLayerSpan,
71+
},
72+
})
73+
this.gapFillSubSolver.setup()
74+
}
75+
76+
// Step the subsolver
77+
if (!this.gapFillSubSolver.solved) {
78+
this.gapFillSubSolver.step()
79+
} else {
80+
// Merge gap-fill results into main state
81+
const gapFillOutput = this.gapFillSubSolver.getOutput()
82+
this.state.placed.push(...gapFillOutput.newPlaced)
83+
84+
// Update placedByLayer
85+
for (const placed of gapFillOutput.newPlaced) {
86+
for (const z of placed.zLayers) {
87+
if (!this.state.placedByLayer[z]) {
88+
this.state.placedByLayer[z] = []
89+
}
90+
this.state.placedByLayer[z]!.push(placed.rect)
91+
}
92+
}
93+
94+
this.state.phase = "DONE"
95+
}
5796
} else if (this.state.phase === "DONE") {
5897
// Finalize once
5998
if (!this.solved) {
@@ -75,7 +114,18 @@ export class RectDiffSolver extends BaseSolver {
75114
if (this.solved || this.state.phase === "DONE") {
76115
return 1
77116
}
78-
return computeProgress(this.state)
117+
118+
const baseProgress = computeProgress(this.state)
119+
120+
// If in GAP_FILL phase, factor in subsolver progress
121+
if (this.state.phase === "GAP_FILL" && this.gapFillSubSolver) {
122+
const gapFillProgress = this.gapFillSubSolver.computeProgress()
123+
// GAP_FILL is the last phase before DONE, so weight it appropriately
124+
// Assume GRID+EXPANSION is 90%, GAP_FILL is remaining 10%
125+
return 0.9 + gapFillProgress * 0.1
126+
}
127+
128+
return baseProgress * 0.9 // Scale down to leave room for GAP_FILL
79129
}
80130

81131
override getOutput(): { meshNodes: CapacityMeshNode[] } {
@@ -129,6 +179,11 @@ export class RectDiffSolver extends BaseSolver {
129179

130180
/** Streaming visualization: board + obstacles + current placements. */
131181
override visualize(): GraphicsObject {
182+
// If in GAP_FILL phase, delegate to subsolver visualization
183+
if (this.state?.phase === "GAP_FILL" && this.gapFillSubSolver) {
184+
return this.gapFillSubSolver.visualize()
185+
}
186+
132187
const rects: NonNullable<GraphicsObject["rects"]> = []
133188
const points: NonNullable<GraphicsObject["points"]> = []
134189
const lines: NonNullable<GraphicsObject["lines"]> = [] // Initialize lines array
@@ -163,17 +218,23 @@ export class RectDiffSolver extends BaseSolver {
163218
})
164219
}
165220

166-
// obstacles (rect & oval as bounding boxes)
221+
// obstacles (rect & oval as bounding boxes) with layer information
167222
for (const obstacle of this.srj.obstacles ?? []) {
168223
if (obstacle.type === "rect" || obstacle.type === "oval") {
224+
// Get layer information if available
225+
const layerInfo =
226+
obstacle.layers && obstacle.layers.length > 0
227+
? `\nz:${obstacle.layers.join(",")}`
228+
: ""
229+
169230
rects.push({
170231
center: { x: obstacle.center.x, y: obstacle.center.y },
171232
width: obstacle.width,
172233
height: obstacle.height,
173234
fill: "#fee2e2",
174235
stroke: "#ef4444",
175236
layer: "obstacle",
176-
label: "obstacle",
237+
label: `obstacle ${layerInfo}`,
177238
})
178239
}
179240
}

lib/solvers/rectdiff/candidates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export function longestFreeSpanAroundZ(params: {
183183
*/
184184
export function computeDefaultGridSizes(bounds: XYRect): number[] {
185185
const ref = Math.max(bounds.width, bounds.height)
186-
return [ref / 8, ref / 16, ref / 32]
186+
return [ref / 32]
187187
}
188188

189189
/**
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// lib/solvers/rectdiff/edge-expansion-gapfill/calculateAvailableSpace.ts
2+
import type { GapFillNode, Direction, EdgeExpansionGapFillState } from "./types"
3+
import type { XYRect } from "../types"
4+
5+
const EPS = 1e-9
6+
7+
export function calculateAvailableSpace(
8+
params: { node: GapFillNode; direction: Direction },
9+
ctx: EdgeExpansionGapFillState,
10+
): number {
11+
const { node, direction } = params
12+
const { bounds, existingPlacedByLayer, obstacles, nodes, newPlaced } = ctx
13+
14+
// Get all potential blockers on the same layers
15+
const blockers: XYRect[] = []
16+
17+
// Add existing placed rects
18+
for (const layer of node.zLayers) {
19+
if (existingPlacedByLayer[layer]) {
20+
blockers.push(...existingPlacedByLayer[layer]!)
21+
}
22+
}
23+
24+
// Add obstacles
25+
for (const layer of node.zLayers) {
26+
if (obstacles[layer]) {
27+
blockers.push(...obstacles[layer]!)
28+
}
29+
}
30+
31+
// Add other gap-fill nodes (already expanded)
32+
for (const otherNode of nodes) {
33+
if (otherNode.id !== node.id) {
34+
blockers.push(otherNode.rect)
35+
}
36+
}
37+
38+
// Add newly placed rects
39+
for (const placed of newPlaced) {
40+
// Check if layers overlap
41+
const hasCommonLayer = node.zLayers.some((z) => placed.zLayers.includes(z))
42+
if (hasCommonLayer) {
43+
blockers.push(placed.rect)
44+
}
45+
}
46+
47+
const rect = node.rect
48+
let maxDistance = Infinity
49+
50+
switch (direction) {
51+
case "up": {
52+
// Expanding upward (increasing y)
53+
maxDistance = bounds.y + bounds.height - (rect.y + rect.height)
54+
55+
for (const obstacle of blockers) {
56+
// Check if obstacle is above and overlaps in x
57+
if (
58+
obstacle.y >= rect.y + rect.height - EPS &&
59+
!(
60+
obstacle.x >= rect.x + rect.width - EPS ||
61+
rect.x >= obstacle.x + obstacle.width - EPS
62+
)
63+
) {
64+
const dist = obstacle.y - (rect.y + rect.height)
65+
maxDistance = Math.min(maxDistance, dist)
66+
}
67+
}
68+
break
69+
}
70+
71+
case "down": {
72+
// Expanding downward (decreasing y)
73+
maxDistance = rect.y - bounds.y
74+
75+
for (const obstacle of blockers) {
76+
// Check if obstacle is below and overlaps in x
77+
if (
78+
obstacle.y + obstacle.height <= rect.y + EPS &&
79+
!(
80+
obstacle.x >= rect.x + rect.width - EPS ||
81+
rect.x >= obstacle.x + obstacle.width - EPS
82+
)
83+
) {
84+
const dist = rect.y - (obstacle.y + obstacle.height)
85+
maxDistance = Math.min(maxDistance, dist)
86+
}
87+
}
88+
break
89+
}
90+
91+
case "right": {
92+
// Expanding rightward (increasing x)
93+
maxDistance = bounds.x + bounds.width - (rect.x + rect.width)
94+
95+
for (const obstacle of blockers) {
96+
// Check if obstacle is to the right and overlaps in y
97+
if (
98+
obstacle.x >= rect.x + rect.width - EPS &&
99+
!(
100+
obstacle.y >= rect.y + rect.height - EPS ||
101+
rect.y >= obstacle.y + obstacle.height - EPS
102+
)
103+
) {
104+
const dist = obstacle.x - (rect.x + rect.width)
105+
maxDistance = Math.min(maxDistance, dist)
106+
}
107+
}
108+
break
109+
}
110+
111+
case "left": {
112+
// Expanding leftward (decreasing x)
113+
maxDistance = rect.x - bounds.x
114+
115+
for (const obstacle of blockers) {
116+
// Check if obstacle is to the left and overlaps in y
117+
if (
118+
obstacle.x + obstacle.width <= rect.x + EPS &&
119+
!(
120+
obstacle.y >= rect.y + rect.height - EPS ||
121+
rect.y >= obstacle.y + obstacle.height - EPS
122+
)
123+
) {
124+
const dist = rect.x - (obstacle.x + obstacle.width)
125+
maxDistance = Math.min(maxDistance, dist)
126+
}
127+
}
128+
break
129+
}
130+
}
131+
132+
return Math.max(0, maxDistance)
133+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// lib/solvers/rectdiff/edge-expansion-gapfill/calculatePotentialArea.ts
2+
import type { GapFillNode, Direction, EdgeExpansionGapFillState } from "./types"
3+
import { calculateAvailableSpace } from "./calculateAvailableSpace"
4+
5+
export function calculatePotentialArea(
6+
params: { node: GapFillNode; direction: Direction },
7+
ctx: EdgeExpansionGapFillState,
8+
): number {
9+
const { node, direction } = params
10+
const available = calculateAvailableSpace({ node, direction }, ctx)
11+
12+
if (direction === "up" || direction === "down") {
13+
return available * node.rect.width
14+
} else {
15+
return available * node.rect.height
16+
}
17+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// lib/solvers/rectdiff/edge-expansion-gapfill/canExpandToLayers.ts
2+
import type { GapFillNode, EdgeExpansionGapFillState } from "./types"
3+
import { rectsOverlap } from "./rectsOverlap"
4+
5+
/**
6+
* Check if a node can expand to include additional layers.
7+
* Returns an array of layers the node can safely span to.
8+
*/
9+
export function canExpandToLayers(params: {
10+
node: GapFillNode
11+
ctx: EdgeExpansionGapFillState
12+
}): number[] {
13+
const { node, ctx } = params
14+
15+
// Don't expand beyond maxMultiLayerSpan if specified
16+
if (ctx.options.maxMultiLayerSpan !== undefined) {
17+
const currentSpan =
18+
Math.max(...node.zLayers) - Math.min(...node.zLayers) + 1
19+
if (currentSpan >= ctx.options.maxMultiLayerSpan) {
20+
return node.zLayers // Already at max span
21+
}
22+
}
23+
24+
const candidateLayers: number[] = []
25+
const minZ = Math.min(...node.zLayers)
26+
const maxZ = Math.max(...node.zLayers)
27+
28+
// Try to expand to adjacent layers
29+
for (let z = 0; z < ctx.layerCount; z++) {
30+
// Skip layers already included
31+
if (node.zLayers.includes(z)) continue
32+
33+
// Only consider adjacent layers
34+
if (z < minZ - 1 || z > maxZ + 1) continue
35+
36+
// Check if this layer has space for the node (no obstacles or existing placed nodes)
37+
let canUse = true
38+
39+
// Check against obstacles on this layer
40+
if (ctx.obstacles[z]) {
41+
for (const obstacle of ctx.obstacles[z]!) {
42+
if (rectsOverlap(node.rect, obstacle)) {
43+
canUse = false
44+
break
45+
}
46+
}
47+
}
48+
49+
// Check against existing placed nodes on this layer
50+
if (canUse && ctx.existingPlacedByLayer[z]) {
51+
for (const placedRect of ctx.existingPlacedByLayer[z]!) {
52+
if (rectsOverlap(node.rect, placedRect)) {
53+
canUse = false
54+
break
55+
}
56+
}
57+
}
58+
59+
if (canUse) {
60+
candidateLayers.push(z)
61+
}
62+
}
63+
64+
// Return all current layers + new candidate layers
65+
const allLayers = [...node.zLayers, ...candidateLayers].sort((a, b) => a - b)
66+
67+
// Check maxMultiLayerSpan again after adding candidates
68+
if (ctx.options.maxMultiLayerSpan !== undefined && allLayers.length > 0) {
69+
const span = Math.max(...allLayers) - Math.min(...allLayers) + 1
70+
if (span > ctx.options.maxMultiLayerSpan) {
71+
// Trim to fit within span
72+
const trimmedLayers = allLayers.slice(0, ctx.options.maxMultiLayerSpan)
73+
return trimmedLayers
74+
}
75+
}
76+
77+
return allLayers
78+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// lib/solvers/rectdiff/edge-expansion-gapfill/computeProgress.ts
2+
import type { EdgeExpansionGapFillState } from "./types"
3+
4+
export function computeProgress(state: EdgeExpansionGapFillState): number {
5+
if (state.phase === "DONE") {
6+
return 1
7+
}
8+
9+
// Find the first non-empty layer to determine obstacle count
10+
let totalObstacles = 0
11+
for (let z = 0; z < state.layerCount; z++) {
12+
if (state.obstacles[z] && state.obstacles[z]!.length > 0) {
13+
totalObstacles = state.obstacles[z]!.length
14+
break
15+
}
16+
}
17+
18+
if (totalObstacles === 0) {
19+
return 1
20+
}
21+
22+
return state.currentObstacleIndex / totalObstacles
23+
}

0 commit comments

Comments
 (0)