@tscircuit/rectdiff 0.0.11 → 0.0.13
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 +163 -27
- package/dist/index.js +1885 -1676
- package/lib/RectDiffPipeline.ts +18 -17
- package/lib/{solvers/rectdiff/types.ts → rectdiff-types.ts} +0 -34
- package/lib/{solvers/rectdiff/visualization.ts → rectdiff-visualization.ts} +2 -1
- package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +9 -12
- package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +1 -1
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +205 -0
- package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +87 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +516 -0
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
- package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
- package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +39 -220
- package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
- package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
- package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +52 -0
- package/lib/utils/buildHardPlacedByLayer.ts +14 -0
- package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
- package/lib/utils/finalizeRects.ts +49 -0
- package/lib/utils/isFullyOccupiedAtPoint.ts +21 -0
- package/lib/utils/rectdiff-geometry.ts +94 -0
- package/lib/utils/resizeSoftOverlaps.ts +74 -0
- package/package.json +1 -1
- package/tests/board-outline.test.ts +2 -1
- package/tests/obstacle-extra-layers.test.ts +1 -1
- package/tests/obstacle-zlayers.test.ts +1 -1
- package/utils/rectsEqual.ts +2 -2
- package/utils/rectsOverlap.ts +2 -2
- package/lib/solvers/RectDiffSolver.ts +0 -231
- package/lib/solvers/rectdiff/engine.ts +0 -481
- /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
// lib/solvers/rectdiff/engine.ts
|
|
2
|
-
import type {
|
|
3
|
-
GridFill3DOptions,
|
|
4
|
-
Placed3D,
|
|
5
|
-
Rect3d,
|
|
6
|
-
RectDiffState,
|
|
7
|
-
XYRect,
|
|
8
|
-
} from "./types"
|
|
9
|
-
import type { SimpleRouteJson } from "../../types/srj-types"
|
|
10
|
-
import {
|
|
11
|
-
computeCandidates3D,
|
|
12
|
-
computeDefaultGridSizes,
|
|
13
|
-
computeEdgeCandidates3D,
|
|
14
|
-
longestFreeSpanAroundZ,
|
|
15
|
-
} from "./candidates"
|
|
16
|
-
import {
|
|
17
|
-
EPS,
|
|
18
|
-
containsPoint,
|
|
19
|
-
expandRectFromSeed,
|
|
20
|
-
overlaps,
|
|
21
|
-
subtractRect2D,
|
|
22
|
-
} from "./geometry"
|
|
23
|
-
import { computeInverseRects } from "./geometry/computeInverseRects"
|
|
24
|
-
import { buildZIndexMap, obstacleToXYRect, obstacleZs } from "./layers"
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Initialize the RectDiff solver state from SimpleRouteJson.
|
|
28
|
-
*/
|
|
29
|
-
export function initState(
|
|
30
|
-
srj: SimpleRouteJson,
|
|
31
|
-
opts: Partial<GridFill3DOptions>,
|
|
32
|
-
): RectDiffState {
|
|
33
|
-
const { layerNames, zIndexByName } = buildZIndexMap(srj)
|
|
34
|
-
const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1)
|
|
35
|
-
|
|
36
|
-
const bounds: XYRect = {
|
|
37
|
-
x: srj.bounds.minX,
|
|
38
|
-
y: srj.bounds.minY,
|
|
39
|
-
width: srj.bounds.maxX - srj.bounds.minX,
|
|
40
|
-
height: srj.bounds.maxY - srj.bounds.minY,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Obstacles per layer
|
|
44
|
-
const obstaclesByLayer: XYRect[][] = Array.from(
|
|
45
|
-
{ length: layerCount },
|
|
46
|
-
() => [],
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
// Compute void rects from outline if present
|
|
50
|
-
let boardVoidRects: XYRect[] = []
|
|
51
|
-
if (srj.outline && srj.outline.length > 2) {
|
|
52
|
-
boardVoidRects = computeInverseRects(bounds, srj.outline)
|
|
53
|
-
// Add void rects as obstacles to ALL layers
|
|
54
|
-
for (const voidR of boardVoidRects) {
|
|
55
|
-
for (let z = 0; z < layerCount; z++) {
|
|
56
|
-
obstaclesByLayer[z]!.push(voidR)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (const obstacle of srj.obstacles ?? []) {
|
|
62
|
-
const rect = obstacleToXYRect(obstacle)
|
|
63
|
-
if (!rect) continue
|
|
64
|
-
const zLayers = obstacleZs(obstacle, zIndexByName)
|
|
65
|
-
const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount)
|
|
66
|
-
if (invalidZs.length) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`RectDiffSolver: obstacle uses z-layer indices ${invalidZs.join(
|
|
69
|
-
",",
|
|
70
|
-
)} outside 0-${layerCount - 1}`,
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
// Persist normalized zLayers back onto the shared SRJ so downstream solvers see them.
|
|
74
|
-
if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length)
|
|
75
|
-
obstacle.zLayers = zLayers
|
|
76
|
-
for (const z of zLayers) obstaclesByLayer[z]!.push(rect)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const trace = Math.max(0.01, srj.minTraceWidth || 0.15)
|
|
80
|
-
const defaults: Required<
|
|
81
|
-
Omit<GridFill3DOptions, "gridSizes" | "maxMultiLayerSpan">
|
|
82
|
-
> & {
|
|
83
|
-
gridSizes: number[]
|
|
84
|
-
maxMultiLayerSpan: number | undefined
|
|
85
|
-
} = {
|
|
86
|
-
gridSizes: computeDefaultGridSizes(bounds),
|
|
87
|
-
initialCellRatio: 0.2,
|
|
88
|
-
maxAspectRatio: 3,
|
|
89
|
-
minSingle: { width: 2 * trace, height: 2 * trace },
|
|
90
|
-
minMulti: {
|
|
91
|
-
width: 4 * trace,
|
|
92
|
-
height: 4 * trace,
|
|
93
|
-
minLayers: Math.min(2, Math.max(1, srj.layerCount || 1)),
|
|
94
|
-
},
|
|
95
|
-
preferMultiLayer: true,
|
|
96
|
-
maxMultiLayerSpan: undefined,
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const options = {
|
|
100
|
-
...defaults,
|
|
101
|
-
...opts,
|
|
102
|
-
gridSizes: opts.gridSizes ?? defaults.gridSizes,
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const placedByLayer: XYRect[][] = Array.from({ length: layerCount }, () => [])
|
|
106
|
-
|
|
107
|
-
// Begin at the **first** grid level; candidates computed lazily on first step
|
|
108
|
-
return {
|
|
109
|
-
srj,
|
|
110
|
-
layerNames,
|
|
111
|
-
layerCount,
|
|
112
|
-
bounds,
|
|
113
|
-
options,
|
|
114
|
-
obstaclesByLayer,
|
|
115
|
-
boardVoidRects,
|
|
116
|
-
phase: "GRID",
|
|
117
|
-
gridIndex: 0,
|
|
118
|
-
candidates: [],
|
|
119
|
-
placed: [],
|
|
120
|
-
placedByLayer,
|
|
121
|
-
expansionIndex: 0,
|
|
122
|
-
edgeAnalysisDone: false,
|
|
123
|
-
totalSeedsThisGrid: 0,
|
|
124
|
-
consumedSeedsThisGrid: 0,
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Build per-layer list of "hard" placed rects (nodes spanning all layers).
|
|
130
|
-
*/
|
|
131
|
-
function buildHardPlacedByLayer(state: RectDiffState): XYRect[][] {
|
|
132
|
-
const out: XYRect[][] = Array.from({ length: state.layerCount }, () => [])
|
|
133
|
-
for (const p of state.placed) {
|
|
134
|
-
if (p.zLayers.length >= state.layerCount) {
|
|
135
|
-
for (const z of p.zLayers) out[z]!.push(p.rect)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return out
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Check if a point is occupied on ALL layers.
|
|
143
|
-
*/
|
|
144
|
-
function isFullyOccupiedAtPoint(
|
|
145
|
-
state: RectDiffState,
|
|
146
|
-
point: { x: number; y: number },
|
|
147
|
-
): boolean {
|
|
148
|
-
for (let z = 0; z < state.layerCount; z++) {
|
|
149
|
-
const obs = state.obstaclesByLayer[z] ?? []
|
|
150
|
-
const placed = state.placedByLayer[z] ?? []
|
|
151
|
-
const occ =
|
|
152
|
-
obs.some((b) => containsPoint(b, point.x, point.y)) ||
|
|
153
|
-
placed.some((b) => containsPoint(b, point.x, point.y))
|
|
154
|
-
if (!occ) return false
|
|
155
|
-
}
|
|
156
|
-
return true
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Shrink/split any soft (non-full-stack) nodes overlapped by the newcomer.
|
|
161
|
-
*/
|
|
162
|
-
function resizeSoftOverlaps(state: RectDiffState, newIndex: number) {
|
|
163
|
-
const newcomer = state.placed[newIndex]!
|
|
164
|
-
const { rect: newR, zLayers: newZs } = newcomer
|
|
165
|
-
const layerCount = state.layerCount
|
|
166
|
-
|
|
167
|
-
const removeIdx: number[] = []
|
|
168
|
-
const toAdd: typeof state.placed = []
|
|
169
|
-
|
|
170
|
-
for (let i = 0; i < state.placed.length; i++) {
|
|
171
|
-
if (i === newIndex) continue
|
|
172
|
-
const old = state.placed[i]!
|
|
173
|
-
// Protect full-stack nodes
|
|
174
|
-
if (old.zLayers.length >= layerCount) continue
|
|
175
|
-
|
|
176
|
-
const sharedZ = old.zLayers.filter((z) => newZs.includes(z))
|
|
177
|
-
if (sharedZ.length === 0) continue
|
|
178
|
-
if (!overlaps(old.rect, newR)) continue
|
|
179
|
-
|
|
180
|
-
// Carve the overlap on the shared layers
|
|
181
|
-
const parts = subtractRect2D(old.rect, newR)
|
|
182
|
-
|
|
183
|
-
// We will replace `old` entirely; re-add unaffected layers (same rect object).
|
|
184
|
-
removeIdx.push(i)
|
|
185
|
-
|
|
186
|
-
const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z))
|
|
187
|
-
if (unaffectedZ.length > 0) {
|
|
188
|
-
toAdd.push({ rect: old.rect, zLayers: unaffectedZ })
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Re-add carved pieces for affected layers, dropping tiny slivers
|
|
192
|
-
const minW = Math.min(
|
|
193
|
-
state.options.minSingle.width,
|
|
194
|
-
state.options.minMulti.width,
|
|
195
|
-
)
|
|
196
|
-
const minH = Math.min(
|
|
197
|
-
state.options.minSingle.height,
|
|
198
|
-
state.options.minMulti.height,
|
|
199
|
-
)
|
|
200
|
-
for (const p of parts) {
|
|
201
|
-
if (p.width + EPS >= minW && p.height + EPS >= minH) {
|
|
202
|
-
toAdd.push({ rect: p, zLayers: sharedZ.slice() })
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Remove (and clear placedByLayer)
|
|
208
|
-
removeIdx
|
|
209
|
-
.sort((a, b) => b - a)
|
|
210
|
-
.forEach((idx) => {
|
|
211
|
-
const rem = state.placed.splice(idx, 1)[0]!
|
|
212
|
-
for (const z of rem.zLayers) {
|
|
213
|
-
const arr = state.placedByLayer[z]!
|
|
214
|
-
const j = arr.findIndex((r) => r === rem.rect)
|
|
215
|
-
if (j >= 0) arr.splice(j, 1)
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
// Add replacements
|
|
220
|
-
for (const p of toAdd) {
|
|
221
|
-
state.placed.push(p)
|
|
222
|
-
for (const z of p.zLayers) state.placedByLayer[z]!.push(p.rect)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* One micro-step during the GRID phase: handle exactly one candidate.
|
|
228
|
-
*/
|
|
229
|
-
export function stepGrid(state: RectDiffState): void {
|
|
230
|
-
const {
|
|
231
|
-
gridSizes,
|
|
232
|
-
initialCellRatio,
|
|
233
|
-
maxAspectRatio,
|
|
234
|
-
minSingle,
|
|
235
|
-
minMulti,
|
|
236
|
-
preferMultiLayer,
|
|
237
|
-
maxMultiLayerSpan,
|
|
238
|
-
} = state.options
|
|
239
|
-
const grid = gridSizes[state.gridIndex]!
|
|
240
|
-
|
|
241
|
-
// Build hard-placed map once per micro-step (cheap)
|
|
242
|
-
const hardPlacedByLayer = buildHardPlacedByLayer(state)
|
|
243
|
-
|
|
244
|
-
// Ensure candidates exist for this grid
|
|
245
|
-
if (state.candidates.length === 0 && state.consumedSeedsThisGrid === 0) {
|
|
246
|
-
state.candidates = computeCandidates3D({
|
|
247
|
-
bounds: state.bounds,
|
|
248
|
-
gridSize: grid,
|
|
249
|
-
layerCount: state.layerCount,
|
|
250
|
-
obstaclesByLayer: state.obstaclesByLayer,
|
|
251
|
-
placedByLayer: state.placedByLayer,
|
|
252
|
-
hardPlacedByLayer,
|
|
253
|
-
})
|
|
254
|
-
state.totalSeedsThisGrid = state.candidates.length
|
|
255
|
-
state.consumedSeedsThisGrid = 0
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// If no candidates remain, advance grid or run edge pass or switch phase
|
|
259
|
-
if (state.candidates.length === 0) {
|
|
260
|
-
if (state.gridIndex + 1 < gridSizes.length) {
|
|
261
|
-
state.gridIndex += 1
|
|
262
|
-
state.totalSeedsThisGrid = 0
|
|
263
|
-
state.consumedSeedsThisGrid = 0
|
|
264
|
-
return
|
|
265
|
-
} else {
|
|
266
|
-
if (!state.edgeAnalysisDone) {
|
|
267
|
-
const minSize = Math.min(minSingle.width, minSingle.height)
|
|
268
|
-
state.candidates = computeEdgeCandidates3D({
|
|
269
|
-
bounds: state.bounds,
|
|
270
|
-
minSize,
|
|
271
|
-
layerCount: state.layerCount,
|
|
272
|
-
obstaclesByLayer: state.obstaclesByLayer,
|
|
273
|
-
placedByLayer: state.placedByLayer,
|
|
274
|
-
hardPlacedByLayer,
|
|
275
|
-
})
|
|
276
|
-
state.edgeAnalysisDone = true
|
|
277
|
-
state.totalSeedsThisGrid = state.candidates.length
|
|
278
|
-
state.consumedSeedsThisGrid = 0
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
state.phase = "EXPANSION"
|
|
282
|
-
state.expansionIndex = 0
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Consume exactly one candidate
|
|
288
|
-
const cand = state.candidates.shift()!
|
|
289
|
-
state.consumedSeedsThisGrid += 1
|
|
290
|
-
|
|
291
|
-
// Evaluate attempts — multi-layer span first (computed ignoring soft nodes)
|
|
292
|
-
const span = longestFreeSpanAroundZ({
|
|
293
|
-
x: cand.x,
|
|
294
|
-
y: cand.y,
|
|
295
|
-
z: cand.z,
|
|
296
|
-
layerCount: state.layerCount,
|
|
297
|
-
minSpan: minMulti.minLayers,
|
|
298
|
-
maxSpan: maxMultiLayerSpan,
|
|
299
|
-
obstaclesByLayer: state.obstaclesByLayer,
|
|
300
|
-
placedByLayer: hardPlacedByLayer,
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
const attempts: Array<{
|
|
304
|
-
kind: "multi" | "single"
|
|
305
|
-
layers: number[]
|
|
306
|
-
minReq: { width: number; height: number }
|
|
307
|
-
}> = []
|
|
308
|
-
|
|
309
|
-
if (span.length >= minMulti.minLayers) {
|
|
310
|
-
attempts.push({
|
|
311
|
-
kind: "multi",
|
|
312
|
-
layers: span,
|
|
313
|
-
minReq: { width: minMulti.width, height: minMulti.height },
|
|
314
|
-
})
|
|
315
|
-
}
|
|
316
|
-
attempts.push({
|
|
317
|
-
kind: "single",
|
|
318
|
-
layers: [cand.z],
|
|
319
|
-
minReq: { width: minSingle.width, height: minSingle.height },
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
const ordered = preferMultiLayer ? attempts : attempts.reverse()
|
|
323
|
-
|
|
324
|
-
for (const attempt of ordered) {
|
|
325
|
-
// HARD blockers only: obstacles on those layers + full-stack nodes
|
|
326
|
-
const hardBlockers: XYRect[] = []
|
|
327
|
-
for (const z of attempt.layers) {
|
|
328
|
-
if (state.obstaclesByLayer[z])
|
|
329
|
-
hardBlockers.push(...state.obstaclesByLayer[z]!)
|
|
330
|
-
if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]!)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const rect = expandRectFromSeed({
|
|
334
|
-
startX: cand.x,
|
|
335
|
-
startY: cand.y,
|
|
336
|
-
gridSize: grid,
|
|
337
|
-
bounds: state.bounds,
|
|
338
|
-
blockers: hardBlockers,
|
|
339
|
-
initialCellRatio,
|
|
340
|
-
maxAspectRatio,
|
|
341
|
-
minReq: attempt.minReq,
|
|
342
|
-
})
|
|
343
|
-
if (!rect) continue
|
|
344
|
-
|
|
345
|
-
// Place the new node
|
|
346
|
-
const placed: Placed3D = { rect, zLayers: [...attempt.layers] }
|
|
347
|
-
const newIndex = state.placed.push(placed) - 1
|
|
348
|
-
for (const z of attempt.layers) state.placedByLayer[z]!.push(rect)
|
|
349
|
-
|
|
350
|
-
// New: carve overlapped soft nodes
|
|
351
|
-
resizeSoftOverlaps(state, newIndex)
|
|
352
|
-
|
|
353
|
-
// New: relax candidate culling — only drop seeds that became fully occupied
|
|
354
|
-
state.candidates = state.candidates.filter(
|
|
355
|
-
(c) => !isFullyOccupiedAtPoint(state, { x: c.x, y: c.y }),
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
return // processed one candidate
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Neither attempt worked; drop this candidate for now.
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* One micro-step during the EXPANSION phase: expand exactly one placed rect.
|
|
366
|
-
*/
|
|
367
|
-
export function stepExpansion(state: RectDiffState): void {
|
|
368
|
-
if (state.expansionIndex >= state.placed.length) {
|
|
369
|
-
// Transition to gap fill phase instead of done
|
|
370
|
-
state.phase = "GAP_FILL"
|
|
371
|
-
return
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const idx = state.expansionIndex
|
|
375
|
-
const p = state.placed[idx]!
|
|
376
|
-
const lastGrid = state.options.gridSizes[state.options.gridSizes.length - 1]!
|
|
377
|
-
|
|
378
|
-
const hardPlacedByLayer = buildHardPlacedByLayer(state)
|
|
379
|
-
|
|
380
|
-
// HARD blockers only: obstacles on p.zLayers + full-stack nodes
|
|
381
|
-
const hardBlockers: XYRect[] = []
|
|
382
|
-
for (const z of p.zLayers) {
|
|
383
|
-
hardBlockers.push(...(state.obstaclesByLayer[z] ?? []))
|
|
384
|
-
hardBlockers.push(...(hardPlacedByLayer[z] ?? []))
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const oldRect = p.rect
|
|
388
|
-
const expanded = expandRectFromSeed({
|
|
389
|
-
startX: p.rect.x + p.rect.width / 2,
|
|
390
|
-
startY: p.rect.y + p.rect.height / 2,
|
|
391
|
-
gridSize: lastGrid,
|
|
392
|
-
bounds: state.bounds,
|
|
393
|
-
blockers: hardBlockers,
|
|
394
|
-
initialCellRatio: 0,
|
|
395
|
-
maxAspectRatio: null,
|
|
396
|
-
minReq: { width: p.rect.width, height: p.rect.height },
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
if (expanded) {
|
|
400
|
-
// Update placement + per-layer index (replace old rect object)
|
|
401
|
-
state.placed[idx] = { rect: expanded, zLayers: p.zLayers }
|
|
402
|
-
for (const z of p.zLayers) {
|
|
403
|
-
const arr = state.placedByLayer[z]!
|
|
404
|
-
const j = arr.findIndex((r) => r === oldRect)
|
|
405
|
-
if (j >= 0) arr[j] = expanded
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Carve overlapped soft neighbors (respect full-stack nodes)
|
|
409
|
-
resizeSoftOverlaps(state, idx)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
state.expansionIndex += 1
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Finalize placed rectangles into output format.
|
|
417
|
-
*/
|
|
418
|
-
export function finalizeRects(state: RectDiffState): Rect3d[] {
|
|
419
|
-
// Convert all placed (free space) nodes to output format
|
|
420
|
-
const out: Rect3d[] = state.placed.map((p) => ({
|
|
421
|
-
minX: p.rect.x,
|
|
422
|
-
minY: p.rect.y,
|
|
423
|
-
maxX: p.rect.x + p.rect.width,
|
|
424
|
-
maxY: p.rect.y + p.rect.height,
|
|
425
|
-
zLayers: [...p.zLayers].sort((a, b) => a - b),
|
|
426
|
-
}))
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Recover obstacles as mesh nodes.
|
|
430
|
-
* Obstacles are stored per-layer in `obstaclesByLayer`, but we want to emit
|
|
431
|
-
* single 3D nodes for multi-layer obstacles if they share the same rect.
|
|
432
|
-
* We use the `XYRect` object reference identity to group layers.
|
|
433
|
-
*/
|
|
434
|
-
const layersByObstacleRect = new Map<XYRect, number[]>()
|
|
435
|
-
|
|
436
|
-
state.obstaclesByLayer.forEach((layerObs, z) => {
|
|
437
|
-
for (const rect of layerObs) {
|
|
438
|
-
const layerIndices = layersByObstacleRect.get(rect) ?? []
|
|
439
|
-
layerIndices.push(z)
|
|
440
|
-
layersByObstacleRect.set(rect, layerIndices)
|
|
441
|
-
}
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
// Append obstacle nodes to the output
|
|
445
|
-
const voidSet = new Set(state.boardVoidRects || [])
|
|
446
|
-
for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
|
|
447
|
-
if (voidSet.has(rect)) continue // Skip void rects
|
|
448
|
-
|
|
449
|
-
out.push({
|
|
450
|
-
minX: rect.x,
|
|
451
|
-
minY: rect.y,
|
|
452
|
-
maxX: rect.x + rect.width,
|
|
453
|
-
maxY: rect.y + rect.height,
|
|
454
|
-
zLayers: layerIndices.sort((a, b) => a - b),
|
|
455
|
-
isObstacle: true,
|
|
456
|
-
})
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return out
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Calculate rough progress number for BaseSolver.progress.
|
|
464
|
-
*/
|
|
465
|
-
export function computeProgress(state: RectDiffState): number {
|
|
466
|
-
const grids = state.options.gridSizes.length
|
|
467
|
-
if (state.phase === "GRID") {
|
|
468
|
-
const g = state.gridIndex
|
|
469
|
-
const base = g / (grids + 1) // reserve final slice for expansion
|
|
470
|
-
const denom = Math.max(1, state.totalSeedsThisGrid)
|
|
471
|
-
const frac = denom ? state.consumedSeedsThisGrid / denom : 1
|
|
472
|
-
return Math.min(0.999, base + frac * (1 / (grids + 1)))
|
|
473
|
-
}
|
|
474
|
-
if (state.phase === "EXPANSION") {
|
|
475
|
-
const base = grids / (grids + 1)
|
|
476
|
-
const denom = Math.max(1, state.placed.length)
|
|
477
|
-
const frac = denom ? state.expansionIndex / denom : 1
|
|
478
|
-
return Math.min(0.999, base + frac * (1 / (grids + 1)))
|
|
479
|
-
}
|
|
480
|
-
return 1
|
|
481
|
-
}
|
|
File without changes
|