@gridspace/raster-path 1.0.4 → 1.0.5
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/build/index.html +1 -1
- package/build/raster-path.js +4 -12
- package/build/raster-worker.js +2450 -0
- package/package.json +8 -4
- package/scripts/build-shaders.js +32 -8
- package/src/core/path-planar.js +788 -0
- package/src/core/path-radial.js +651 -0
- package/src/core/raster-config.js +185 -0
- package/src/{index.js → core/raster-path.js} +4 -12
- package/src/core/raster-planar.js +754 -0
- package/src/core/raster-tool.js +104 -0
- package/src/core/raster-worker.js +152 -0
- package/src/core/workload-calibrate.js +416 -0
- package/src/shaders/workload-calibrate.wgsl +106 -0
- package/src/test/calibrate-test.cjs +136 -0
- package/src/test/extreme-work-test.cjs +167 -0
- package/src/test/radial-thread-limit-test.cjs +152 -0
- package/src/web/index.html +1 -1
- package/build/webgpu-worker.js +0 -2800
- package/src/web/webgpu-worker.js +0 -2303
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
* Path Planar - Planar Toolpath Generation
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
*
|
|
6
|
+
* Generates CNC toolpaths by scanning a sparse tool representation over a
|
|
7
|
+
* dense terrain height map. Computes Z-heights where tool contacts terrain.
|
|
8
|
+
*
|
|
9
|
+
* EXPORTS:
|
|
10
|
+
* ────────
|
|
11
|
+
* Functions:
|
|
12
|
+
* - generateToolpath(terrainPoints, toolPoints, xStep, yStep, zFloor, ...)
|
|
13
|
+
* Main API - generates toolpath with automatic tiling
|
|
14
|
+
* - generateToolpathWithSparseTools(terrainPoints, sparseToolData, ...)
|
|
15
|
+
* Batch-optimized version with pre-created sparse tool
|
|
16
|
+
* - createReusableToolpathBuffers(width, height, sparseToolData, ...)
|
|
17
|
+
* Create GPU buffers for reuse across multiple tiles
|
|
18
|
+
* - destroyReusableToolpathBuffers(buffers)
|
|
19
|
+
* Cleanup GPU buffers
|
|
20
|
+
* - runToolpathComputeWithBuffers(terrainData, ...)
|
|
21
|
+
* Run toolpath compute shader with pre-allocated buffers
|
|
22
|
+
*
|
|
23
|
+
* ALGORITHM:
|
|
24
|
+
* ──────────
|
|
25
|
+
* For each output point (i, j) sampled at (xStep, yStep):
|
|
26
|
+
* 1. Position tool center at terrain grid cell (i, j)
|
|
27
|
+
* 2. For each point in sparse tool:
|
|
28
|
+
* - Calculate terrain sample position: terrain[i + xOffset, j + yOffset]
|
|
29
|
+
* - Calculate tool collision Z: terrainZ - toolZ
|
|
30
|
+
* 3. Output maximum collision Z (highest point tool must be raised)
|
|
31
|
+
*
|
|
32
|
+
* TILING SUPPORT:
|
|
33
|
+
* ───────────────
|
|
34
|
+
* For large terrains exceeding GPU memory:
|
|
35
|
+
* 1. Calculate tool dimensions to determine required overlap
|
|
36
|
+
* 2. Subdivide terrain into tiles with tool-radius overlap
|
|
37
|
+
* 3. Pre-generate all tile terrain arrays (CPU side)
|
|
38
|
+
* 4. Create reusable GPU buffers sized for largest tile
|
|
39
|
+
* 5. Process each tile, reusing buffers
|
|
40
|
+
* 6. Stitch results, dropping overlap regions
|
|
41
|
+
*
|
|
42
|
+
* MEMORY OPTIMIZATION:
|
|
43
|
+
* ────────────────────
|
|
44
|
+
* - Tool buffer created once, reused for all tiles
|
|
45
|
+
* - Terrain buffer updated per tile (GPU DMA)
|
|
46
|
+
* - Output buffer reused, read back per tile
|
|
47
|
+
* - Reduces GPU memory pressure by 10-100x for large models
|
|
48
|
+
*
|
|
49
|
+
* OUTPUT FORMAT:
|
|
50
|
+
* ──────────────
|
|
51
|
+
* Float32Array of Z-heights, row-major order:
|
|
52
|
+
* [z(0,0), z(1,0), z(2,0), ..., z(0,1), z(1,1), ...]
|
|
53
|
+
* Dimensions: (terrain.width / xStep) × (terrain.height / yStep)
|
|
54
|
+
*
|
|
55
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
device, deviceCapabilities, isInitialized, config,
|
|
60
|
+
cachedToolpathPipeline, EMPTY_CELL, debug, initWebGPU
|
|
61
|
+
} from './raster-config.js';
|
|
62
|
+
import { createHeightMapFromPoints } from './raster-planar.js';
|
|
63
|
+
import { createSparseToolFromPoints } from './raster-tool.js';
|
|
64
|
+
|
|
65
|
+
// Generate toolpath with pre-created sparse tool (for batch operations)
|
|
66
|
+
export async function generateToolpathWithSparseTools(terrainPoints, sparseToolData, xStep, yStep, oobZ, gridStep, terrainBounds = null, singleScanline = false) {
|
|
67
|
+
const startTime = performance.now();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Create height map from terrain points (use terrain bounds if provided)
|
|
71
|
+
const terrainMapData = createHeightMapFromPoints(terrainPoints, gridStep, terrainBounds);
|
|
72
|
+
|
|
73
|
+
// Run WebGPU compute with pre-created sparse tool
|
|
74
|
+
const result = await runToolpathCompute(
|
|
75
|
+
terrainMapData, sparseToolData, xStep, yStep, oobZ, startTime
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
debug.error('Error generating toolpath:', error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Generate toolpath for a single region (internal)
|
|
86
|
+
async function generateToolpathSingle(terrainPoints, toolPoints, xStep, yStep, oobZ, gridStep, terrainBounds = null) {
|
|
87
|
+
const startTime = performance.now();
|
|
88
|
+
debug.log('Generating toolpath...');
|
|
89
|
+
debug.log(`Input: terrain ${terrainPoints.length/3} points, tool ${toolPoints.length/3} points, steps (${xStep}, ${yStep}), oobZ ${oobZ}, gridStep ${gridStep}`);
|
|
90
|
+
|
|
91
|
+
if (terrainBounds) {
|
|
92
|
+
debug.log(`Using terrain bounds: min(${terrainBounds.min.x.toFixed(2)}, ${terrainBounds.min.y.toFixed(2)}, ${terrainBounds.min.z.toFixed(2)}) max(${terrainBounds.max.x.toFixed(2)}, ${terrainBounds.max.y.toFixed(2)}, ${terrainBounds.max.z.toFixed(2)})`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Create height map from terrain points (use terrain bounds if provided)
|
|
97
|
+
const terrainMapData = createHeightMapFromPoints(terrainPoints, gridStep, terrainBounds);
|
|
98
|
+
debug.log(`Created terrain map: ${terrainMapData.width}x${terrainMapData.height}`);
|
|
99
|
+
|
|
100
|
+
// Create sparse tool representation
|
|
101
|
+
const sparseToolData = createSparseToolFromPoints(toolPoints);
|
|
102
|
+
debug.log(`Created sparse tool: ${sparseToolData.count} points`);
|
|
103
|
+
|
|
104
|
+
// Run WebGPU compute
|
|
105
|
+
const result = await runToolpathCompute(
|
|
106
|
+
terrainMapData, sparseToolData, xStep, yStep, oobZ, startTime
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
debug.error('Error generating toolpath:', error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runToolpathCompute(terrainMapData, sparseToolData, xStep, yStep, oobZ, startTime) {
|
|
117
|
+
if (!isInitialized) {
|
|
118
|
+
const success = await initWebGPU();
|
|
119
|
+
if (!success) {
|
|
120
|
+
throw new Error('WebGPU not available');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Use WASM-generated terrain grid
|
|
125
|
+
const terrainBuffer = device.createBuffer({
|
|
126
|
+
size: terrainMapData.grid.byteLength,
|
|
127
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
128
|
+
});
|
|
129
|
+
device.queue.writeBuffer(terrainBuffer, 0, terrainMapData.grid);
|
|
130
|
+
|
|
131
|
+
// Use WASM-generated sparse tool
|
|
132
|
+
const toolBufferData = new ArrayBuffer(sparseToolData.count * 16);
|
|
133
|
+
const toolBufferI32 = new Int32Array(toolBufferData);
|
|
134
|
+
const toolBufferF32 = new Float32Array(toolBufferData);
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < sparseToolData.count; i++) {
|
|
137
|
+
toolBufferI32[i * 4 + 0] = sparseToolData.xOffsets[i];
|
|
138
|
+
toolBufferI32[i * 4 + 1] = sparseToolData.yOffsets[i];
|
|
139
|
+
toolBufferF32[i * 4 + 2] = sparseToolData.zValues[i];
|
|
140
|
+
toolBufferF32[i * 4 + 3] = 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toolBuffer = device.createBuffer({
|
|
144
|
+
size: toolBufferData.byteLength,
|
|
145
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
146
|
+
});
|
|
147
|
+
device.queue.writeBuffer(toolBuffer, 0, toolBufferData);
|
|
148
|
+
|
|
149
|
+
// Calculate output dimensions
|
|
150
|
+
const pointsPerLine = Math.ceil(terrainMapData.width / xStep);
|
|
151
|
+
const numScanlines = Math.ceil(terrainMapData.height / yStep);
|
|
152
|
+
const outputSize = pointsPerLine * numScanlines;
|
|
153
|
+
|
|
154
|
+
const outputBuffer = device.createBuffer({
|
|
155
|
+
size: outputSize * 4,
|
|
156
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const uniformData = new Uint32Array([
|
|
160
|
+
terrainMapData.width,
|
|
161
|
+
terrainMapData.height,
|
|
162
|
+
sparseToolData.count,
|
|
163
|
+
xStep,
|
|
164
|
+
yStep,
|
|
165
|
+
0,
|
|
166
|
+
pointsPerLine,
|
|
167
|
+
numScanlines,
|
|
168
|
+
0, // y_offset (default 0 for planar mode)
|
|
169
|
+
]);
|
|
170
|
+
const uniformDataFloat = new Float32Array(uniformData.buffer);
|
|
171
|
+
uniformDataFloat[5] = oobZ;
|
|
172
|
+
|
|
173
|
+
const uniformBuffer = device.createBuffer({
|
|
174
|
+
size: uniformData.byteLength,
|
|
175
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
176
|
+
});
|
|
177
|
+
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
|
|
178
|
+
|
|
179
|
+
// CRITICAL: Wait for all writeBuffer operations to complete before compute dispatch
|
|
180
|
+
await device.queue.onSubmittedWorkDone();
|
|
181
|
+
|
|
182
|
+
// Use cached pipeline
|
|
183
|
+
const bindGroup = device.createBindGroup({
|
|
184
|
+
layout: cachedToolpathPipeline.getBindGroupLayout(0),
|
|
185
|
+
entries: [
|
|
186
|
+
{ binding: 0, resource: { buffer: terrainBuffer } },
|
|
187
|
+
{ binding: 1, resource: { buffer: toolBuffer } },
|
|
188
|
+
{ binding: 2, resource: { buffer: outputBuffer } },
|
|
189
|
+
{ binding: 3, resource: { buffer: uniformBuffer } },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const commandEncoder = device.createCommandEncoder();
|
|
194
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
195
|
+
passEncoder.setPipeline(cachedToolpathPipeline);
|
|
196
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
197
|
+
|
|
198
|
+
const workgroupsX = Math.ceil(pointsPerLine / 16);
|
|
199
|
+
const workgroupsY = Math.ceil(numScanlines / 16);
|
|
200
|
+
passEncoder.dispatchWorkgroups(workgroupsX, workgroupsY);
|
|
201
|
+
passEncoder.end();
|
|
202
|
+
|
|
203
|
+
const stagingBuffer = device.createBuffer({
|
|
204
|
+
size: outputSize * 4,
|
|
205
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputSize * 4);
|
|
209
|
+
|
|
210
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
211
|
+
|
|
212
|
+
// CRITICAL: Wait for GPU to finish before reading results
|
|
213
|
+
await device.queue.onSubmittedWorkDone();
|
|
214
|
+
|
|
215
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
216
|
+
|
|
217
|
+
const outputData = new Float32Array(stagingBuffer.getMappedRange());
|
|
218
|
+
const result = new Float32Array(outputData);
|
|
219
|
+
stagingBuffer.unmap();
|
|
220
|
+
|
|
221
|
+
terrainBuffer.destroy();
|
|
222
|
+
toolBuffer.destroy();
|
|
223
|
+
outputBuffer.destroy();
|
|
224
|
+
uniformBuffer.destroy();
|
|
225
|
+
stagingBuffer.destroy();
|
|
226
|
+
|
|
227
|
+
const endTime = performance.now();
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
pathData: result,
|
|
231
|
+
numScanlines,
|
|
232
|
+
pointsPerLine,
|
|
233
|
+
generationTime: endTime - startTime
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Create reusable GPU buffers for tiled toolpath generation
|
|
238
|
+
export function createReusableToolpathBuffers(terrainWidth, terrainHeight, sparseToolData, xStep, yStep) {
|
|
239
|
+
const pointsPerLine = Math.ceil(terrainWidth / xStep);
|
|
240
|
+
const numScanlines = Math.ceil(terrainHeight / yStep);
|
|
241
|
+
const outputSize = pointsPerLine * numScanlines;
|
|
242
|
+
|
|
243
|
+
// Create terrain buffer (will be updated for each tile)
|
|
244
|
+
const terrainBuffer = device.createBuffer({
|
|
245
|
+
size: terrainWidth * terrainHeight * 4,
|
|
246
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Create tool buffer (STATIC - same for all tiles!)
|
|
250
|
+
const toolBufferData = new ArrayBuffer(sparseToolData.count * 16);
|
|
251
|
+
const toolBufferI32 = new Int32Array(toolBufferData);
|
|
252
|
+
const toolBufferF32 = new Float32Array(toolBufferData);
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < sparseToolData.count; i++) {
|
|
255
|
+
toolBufferI32[i * 4 + 0] = sparseToolData.xOffsets[i];
|
|
256
|
+
toolBufferI32[i * 4 + 1] = sparseToolData.yOffsets[i];
|
|
257
|
+
toolBufferF32[i * 4 + 2] = sparseToolData.zValues[i];
|
|
258
|
+
toolBufferF32[i * 4 + 3] = 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const toolBuffer = device.createBuffer({
|
|
262
|
+
size: toolBufferData.byteLength,
|
|
263
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
264
|
+
});
|
|
265
|
+
device.queue.writeBuffer(toolBuffer, 0, toolBufferData); // Write once!
|
|
266
|
+
|
|
267
|
+
// Create output buffer (will be read for each tile)
|
|
268
|
+
const outputBuffer = device.createBuffer({
|
|
269
|
+
size: outputSize * 4,
|
|
270
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Create uniform buffer (will be updated for each tile)
|
|
274
|
+
const uniformBuffer = device.createBuffer({
|
|
275
|
+
size: 36, // 9 fields × 4 bytes (added y_offset field)
|
|
276
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Create staging buffer (will be reused for readback)
|
|
280
|
+
const stagingBuffer = device.createBuffer({
|
|
281
|
+
size: outputSize * 4,
|
|
282
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
terrainBuffer,
|
|
287
|
+
toolBuffer,
|
|
288
|
+
outputBuffer,
|
|
289
|
+
uniformBuffer,
|
|
290
|
+
stagingBuffer,
|
|
291
|
+
maxOutputSize: outputSize,
|
|
292
|
+
maxTerrainWidth: terrainWidth,
|
|
293
|
+
maxTerrainHeight: terrainHeight,
|
|
294
|
+
sparseToolData
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Destroy reusable GPU buffers
|
|
299
|
+
export function destroyReusableToolpathBuffers(buffers) {
|
|
300
|
+
buffers.terrainBuffer.destroy();
|
|
301
|
+
buffers.toolBuffer.destroy();
|
|
302
|
+
buffers.outputBuffer.destroy();
|
|
303
|
+
buffers.uniformBuffer.destroy();
|
|
304
|
+
buffers.stagingBuffer.destroy();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Run toolpath compute using pre-created reusable buffers
|
|
308
|
+
export async function runToolpathComputeWithBuffers(terrainData, terrainWidth, terrainHeight, xStep, yStep, oobZ, buffers, startTime) {
|
|
309
|
+
// Update terrain buffer with new tile data
|
|
310
|
+
device.queue.writeBuffer(buffers.terrainBuffer, 0, terrainData);
|
|
311
|
+
|
|
312
|
+
// Calculate output dimensions
|
|
313
|
+
const pointsPerLine = Math.ceil(terrainWidth / xStep);
|
|
314
|
+
const numScanlines = Math.ceil(terrainHeight / yStep);
|
|
315
|
+
const outputSize = pointsPerLine * numScanlines;
|
|
316
|
+
|
|
317
|
+
// Calculate Y offset for single-scanline radial mode
|
|
318
|
+
// When numScanlines=1 and terrainHeight > 1, center the tool at the midline
|
|
319
|
+
const yOffset = (numScanlines === 1 && terrainHeight > 1) ? Math.floor(terrainHeight / 2) : 0;
|
|
320
|
+
|
|
321
|
+
// Update uniforms for this tile
|
|
322
|
+
const uniformData = new Uint32Array([
|
|
323
|
+
terrainWidth,
|
|
324
|
+
terrainHeight,
|
|
325
|
+
buffers.sparseToolData.count,
|
|
326
|
+
xStep,
|
|
327
|
+
yStep,
|
|
328
|
+
0,
|
|
329
|
+
pointsPerLine,
|
|
330
|
+
numScanlines,
|
|
331
|
+
yOffset, // y_offset for radial single-scanline mode
|
|
332
|
+
]);
|
|
333
|
+
const uniformDataFloat = new Float32Array(uniformData.buffer);
|
|
334
|
+
uniformDataFloat[5] = oobZ;
|
|
335
|
+
device.queue.writeBuffer(buffers.uniformBuffer, 0, uniformData);
|
|
336
|
+
|
|
337
|
+
// CRITICAL: Wait for all writeBuffer operations to complete before compute dispatch
|
|
338
|
+
// Without this, compute shader may read stale/incomplete buffer data
|
|
339
|
+
await device.queue.onSubmittedWorkDone();
|
|
340
|
+
|
|
341
|
+
// Create bind group (reusing cached pipeline)
|
|
342
|
+
const bindGroup = device.createBindGroup({
|
|
343
|
+
layout: cachedToolpathPipeline.getBindGroupLayout(0),
|
|
344
|
+
entries: [
|
|
345
|
+
{ binding: 0, resource: { buffer: buffers.terrainBuffer } },
|
|
346
|
+
{ binding: 1, resource: { buffer: buffers.toolBuffer } },
|
|
347
|
+
{ binding: 2, resource: { buffer: buffers.outputBuffer } },
|
|
348
|
+
{ binding: 3, resource: { buffer: buffers.uniformBuffer } },
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Dispatch compute shader
|
|
353
|
+
const commandEncoder = device.createCommandEncoder();
|
|
354
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
355
|
+
passEncoder.setPipeline(cachedToolpathPipeline);
|
|
356
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
357
|
+
|
|
358
|
+
const workgroupsX = Math.ceil(pointsPerLine / 16);
|
|
359
|
+
const workgroupsY = Math.ceil(numScanlines / 16);
|
|
360
|
+
passEncoder.dispatchWorkgroups(workgroupsX, workgroupsY);
|
|
361
|
+
passEncoder.end();
|
|
362
|
+
|
|
363
|
+
// Copy to staging buffer
|
|
364
|
+
commandEncoder.copyBufferToBuffer(buffers.outputBuffer, 0, buffers.stagingBuffer, 0, outputSize * 4);
|
|
365
|
+
|
|
366
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
367
|
+
|
|
368
|
+
// CRITICAL: Wait for GPU to finish before reading results
|
|
369
|
+
await device.queue.onSubmittedWorkDone();
|
|
370
|
+
|
|
371
|
+
await buffers.stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
372
|
+
|
|
373
|
+
// Create a true copy using slice() - new Float32Array(typedArray) only creates a view!
|
|
374
|
+
const outputData = new Float32Array(buffers.stagingBuffer.getMappedRange(), 0, outputSize);
|
|
375
|
+
const result = outputData.slice(); // slice() creates a new ArrayBuffer with copied data
|
|
376
|
+
buffers.stagingBuffer.unmap();
|
|
377
|
+
|
|
378
|
+
const endTime = performance.now();
|
|
379
|
+
|
|
380
|
+
// Debug: Log first few Z values to detect non-determinism
|
|
381
|
+
if (result.length > 0) {
|
|
382
|
+
const samples = [];
|
|
383
|
+
for (let i = 0; i < Math.min(10, result.length); i++) {
|
|
384
|
+
samples.push(result[i].toFixed(3));
|
|
385
|
+
}
|
|
386
|
+
// debug.log(`[Toolpath] Output samples (${result.length} total): ${samples.join(', ')}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
pathData: result,
|
|
391
|
+
numScanlines,
|
|
392
|
+
pointsPerLine,
|
|
393
|
+
generationTime: endTime - startTime
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Generate toolpath with tiling support (public API)
|
|
398
|
+
export async function generateToolpath(terrainPoints, toolPoints, xStep, yStep, oobZ, gridStep, terrainBounds = null, singleScanline = false) {
|
|
399
|
+
// Calculate bounds if not provided
|
|
400
|
+
if (!terrainBounds) {
|
|
401
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
402
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
403
|
+
for (let i = 0; i < terrainPoints.length; i += 3) {
|
|
404
|
+
minX = Math.min(minX, terrainPoints[i]);
|
|
405
|
+
maxX = Math.max(maxX, terrainPoints[i]);
|
|
406
|
+
minY = Math.min(minY, terrainPoints[i + 1]);
|
|
407
|
+
maxY = Math.max(maxY, terrainPoints[i + 1]);
|
|
408
|
+
minZ = Math.min(minZ, terrainPoints[i + 2]);
|
|
409
|
+
maxZ = Math.max(maxZ, terrainPoints[i + 2]);
|
|
410
|
+
}
|
|
411
|
+
terrainBounds = {
|
|
412
|
+
min: { x: minX, y: minY, z: minZ },
|
|
413
|
+
max: { x: maxX, y: maxY, z: maxZ }
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Note: singleScanline mode means OUTPUT only centerline, but terrain bounds stay full
|
|
418
|
+
// This ensures all terrain Y values contribute to tool interference at the centerline
|
|
419
|
+
|
|
420
|
+
// Debug tool bounds and center
|
|
421
|
+
for (let i=0; i<toolPoints.length; i += 3) {
|
|
422
|
+
if (toolPoints[i] === 0 && toolPoints[i+1] === 0) {
|
|
423
|
+
debug.log('[WebGPU Worker]', { TOOL_CENTER: toolPoints[i+2] });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
debug.log('[WebGPU Worker]',
|
|
427
|
+
'toolZMin:', ([...toolPoints].filter((_,i) => i % 3 === 2).reduce((a,b) => Math.min(a,b), Infinity)),
|
|
428
|
+
'toolZMax:', ([...toolPoints].filter((_,i) => i % 3 === 2).reduce((a,b) => Math.max(a,b), -Infinity))
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Calculate tool dimensions for overlap
|
|
432
|
+
// Tool points are [gridX, gridY, Z] where X/Y are grid indices (not mm)
|
|
433
|
+
let toolMinX = Infinity, toolMaxX = -Infinity;
|
|
434
|
+
let toolMinY = Infinity, toolMaxY = -Infinity;
|
|
435
|
+
for (let i = 0; i < toolPoints.length; i += 3) {
|
|
436
|
+
toolMinX = Math.min(toolMinX, toolPoints[i]);
|
|
437
|
+
toolMaxX = Math.max(toolMaxX, toolPoints[i]);
|
|
438
|
+
toolMinY = Math.min(toolMinY, toolPoints[i + 1]);
|
|
439
|
+
toolMaxY = Math.max(toolMaxY, toolPoints[i + 1]);
|
|
440
|
+
}
|
|
441
|
+
// Tool dimensions in grid cells
|
|
442
|
+
const toolWidthCells = toolMaxX - toolMinX;
|
|
443
|
+
const toolHeightCells = toolMaxY - toolMinY;
|
|
444
|
+
// Convert to mm for logging
|
|
445
|
+
const toolWidthMm = toolWidthCells * gridStep;
|
|
446
|
+
const toolHeightMm = toolHeightCells * gridStep;
|
|
447
|
+
|
|
448
|
+
// Check if tiling is needed based on output grid size
|
|
449
|
+
const outputWidth = Math.ceil((terrainBounds.max.x - terrainBounds.min.x) / gridStep) + 1;
|
|
450
|
+
const outputHeight = Math.ceil((terrainBounds.max.y - terrainBounds.min.y) / gridStep) + 1;
|
|
451
|
+
const outputPoints = Math.ceil(outputWidth / xStep) * Math.ceil(outputHeight / yStep);
|
|
452
|
+
const outputMemory = outputPoints * 4; // 4 bytes per float
|
|
453
|
+
|
|
454
|
+
const configuredLimit = config.maxGPUMemoryMB * 1024 * 1024;
|
|
455
|
+
const deviceLimit = deviceCapabilities.maxStorageBufferBindingSize;
|
|
456
|
+
const maxSafeSize = Math.min(configuredLimit, deviceLimit) * config.gpuMemorySafetyMargin;
|
|
457
|
+
|
|
458
|
+
if (outputMemory <= maxSafeSize) {
|
|
459
|
+
// No tiling needed
|
|
460
|
+
return await generateToolpathSingle(terrainPoints, toolPoints, xStep, yStep, oobZ, gridStep, terrainBounds);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Tiling needed (terrain is ALWAYS dense)
|
|
464
|
+
const tilingStartTime = performance.now();
|
|
465
|
+
debug.log('Using tiled toolpath generation');
|
|
466
|
+
debug.log(`Terrain: DENSE (${terrainPoints.length} cells = ${outputWidth}x${outputHeight})`);
|
|
467
|
+
debug.log(`Tool dimensions: ${toolWidthMm.toFixed(2)}mm × ${toolHeightMm.toFixed(2)}mm (${toolWidthCells}×${toolHeightCells} cells)`);
|
|
468
|
+
|
|
469
|
+
// Create tiles with tool-size overlap (pass dimensions in grid cells)
|
|
470
|
+
const { tiles, maxTileGridWidth, maxTileGridHeight } = createToolpathTiles(terrainBounds, gridStep, xStep, yStep, toolWidthCells, toolHeightCells, maxSafeSize);
|
|
471
|
+
debug.log(`Created ${tiles.length} tiles`);
|
|
472
|
+
|
|
473
|
+
// Pre-generate all tile terrain point arrays
|
|
474
|
+
const pregenStartTime = performance.now();
|
|
475
|
+
debug.log(`Pre-generating ${tiles.length} tile terrain arrays...`);
|
|
476
|
+
const allTileTerrainPoints = [];
|
|
477
|
+
|
|
478
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
479
|
+
const tile = tiles[i];
|
|
480
|
+
|
|
481
|
+
// Extract terrain sub-grid for this tile (terrain is ALWAYS dense)
|
|
482
|
+
const tileMinGridX = Math.floor((tile.bounds.min.x - terrainBounds.min.x) / gridStep);
|
|
483
|
+
const tileMaxGridX = Math.ceil((tile.bounds.max.x - terrainBounds.min.x) / gridStep);
|
|
484
|
+
const tileMinGridY = Math.floor((tile.bounds.min.y - terrainBounds.min.y) / gridStep);
|
|
485
|
+
const tileMaxGridY = Math.ceil((tile.bounds.max.y - terrainBounds.min.y) / gridStep);
|
|
486
|
+
|
|
487
|
+
const tileWidth = tileMaxGridX - tileMinGridX + 1;
|
|
488
|
+
const tileHeight = tileMaxGridY - tileMinGridY + 1;
|
|
489
|
+
|
|
490
|
+
// Pad to max dimensions for consistent buffer sizing
|
|
491
|
+
const paddedTileTerrainPoints = new Float32Array(maxTileGridWidth * maxTileGridHeight);
|
|
492
|
+
paddedTileTerrainPoints.fill(EMPTY_CELL);
|
|
493
|
+
|
|
494
|
+
// Copy relevant sub-grid from full terrain into top-left of padded array
|
|
495
|
+
for (let ty = 0; ty < tileHeight; ty++) {
|
|
496
|
+
const globalY = tileMinGridY + ty;
|
|
497
|
+
if (globalY < 0 || globalY >= outputHeight) continue;
|
|
498
|
+
|
|
499
|
+
for (let tx = 0; tx < tileWidth; tx++) {
|
|
500
|
+
const globalX = tileMinGridX + tx;
|
|
501
|
+
if (globalX < 0 || globalX >= outputWidth) continue;
|
|
502
|
+
|
|
503
|
+
const globalIdx = globalY * outputWidth + globalX;
|
|
504
|
+
const tileIdx = ty * maxTileGridWidth + tx; // Use maxTileGridWidth for stride
|
|
505
|
+
paddedTileTerrainPoints[tileIdx] = terrainPoints[globalIdx];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
allTileTerrainPoints.push({
|
|
510
|
+
data: paddedTileTerrainPoints,
|
|
511
|
+
actualWidth: tileWidth,
|
|
512
|
+
actualHeight: tileHeight
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const pregenTime = performance.now() - pregenStartTime;
|
|
517
|
+
debug.log(`Pre-generation complete in ${pregenTime.toFixed(1)}ms`);
|
|
518
|
+
|
|
519
|
+
// Create reusable GPU buffers (sized for maximum tile)
|
|
520
|
+
if (!isInitialized) {
|
|
521
|
+
const success = await initWebGPU();
|
|
522
|
+
if (!success) {
|
|
523
|
+
throw new Error('WebGPU not available');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const sparseToolData = createSparseToolFromPoints(toolPoints);
|
|
528
|
+
const reusableBuffers = createReusableToolpathBuffers(maxTileGridWidth, maxTileGridHeight, sparseToolData, xStep, yStep);
|
|
529
|
+
debug.log(`Created reusable GPU buffers for ${maxTileGridWidth}x${maxTileGridHeight} tiles`);
|
|
530
|
+
|
|
531
|
+
// Process each tile with reusable buffers
|
|
532
|
+
const tileResults = [];
|
|
533
|
+
let totalTileTime = 0;
|
|
534
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
535
|
+
const tile = tiles[i];
|
|
536
|
+
const tileStartTime = performance.now();
|
|
537
|
+
debug.log(`Processing tile ${i + 1}/${tiles.length}...`);
|
|
538
|
+
|
|
539
|
+
// Report progress
|
|
540
|
+
const percent = Math.round(((i + 1) / tiles.length) * 100);
|
|
541
|
+
self.postMessage({
|
|
542
|
+
type: 'toolpath-progress',
|
|
543
|
+
data: {
|
|
544
|
+
percent,
|
|
545
|
+
current: i + 1,
|
|
546
|
+
total: tiles.length,
|
|
547
|
+
layer: i + 1 // Using tile index as "layer" for consistency
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
debug.log(`Tile ${i+1} using pre-generated terrain: ${allTileTerrainPoints[i].actualWidth}x${allTileTerrainPoints[i].actualHeight} (padded to ${maxTileGridWidth}x${maxTileGridHeight})`);
|
|
552
|
+
|
|
553
|
+
// Generate toolpath for this tile using reusable buffers
|
|
554
|
+
const tileToolpathResult = await runToolpathComputeWithBuffers(
|
|
555
|
+
allTileTerrainPoints[i].data,
|
|
556
|
+
maxTileGridWidth,
|
|
557
|
+
maxTileGridHeight,
|
|
558
|
+
xStep,
|
|
559
|
+
yStep,
|
|
560
|
+
oobZ,
|
|
561
|
+
reusableBuffers,
|
|
562
|
+
tileStartTime
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const tileTime = performance.now() - tileStartTime;
|
|
566
|
+
totalTileTime += tileTime;
|
|
567
|
+
|
|
568
|
+
tileResults.push({
|
|
569
|
+
pathData: tileToolpathResult.pathData,
|
|
570
|
+
numScanlines: tileToolpathResult.numScanlines,
|
|
571
|
+
pointsPerLine: tileToolpathResult.pointsPerLine,
|
|
572
|
+
tile: tile
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
debug.log(`Tile ${i + 1}/${tiles.length} complete: ${tileToolpathResult.numScanlines}×${tileToolpathResult.pointsPerLine} in ${tileTime.toFixed(1)}ms`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Cleanup reusable buffers
|
|
579
|
+
destroyReusableToolpathBuffers(reusableBuffers);
|
|
580
|
+
|
|
581
|
+
debug.log(`All tiles processed in ${totalTileTime.toFixed(1)}ms (avg ${(totalTileTime/tiles.length).toFixed(1)}ms per tile)`);
|
|
582
|
+
|
|
583
|
+
// Stitch tiles together, dropping overlap regions
|
|
584
|
+
const stitchStartTime = performance.now();
|
|
585
|
+
const stitchedResult = stitchToolpathTiles(tileResults, terrainBounds, gridStep, xStep, yStep);
|
|
586
|
+
const stitchTime = performance.now() - stitchStartTime;
|
|
587
|
+
|
|
588
|
+
const totalTime = performance.now() - tilingStartTime;
|
|
589
|
+
debug.log(`Stitching took ${stitchTime.toFixed(1)}ms`);
|
|
590
|
+
debug.log(`Tiled toolpath complete: ${stitchedResult.numScanlines}×${stitchedResult.pointsPerLine} in ${totalTime.toFixed(1)}ms total`);
|
|
591
|
+
|
|
592
|
+
// Update generation time to reflect total tiled time
|
|
593
|
+
stitchedResult.generationTime = totalTime;
|
|
594
|
+
|
|
595
|
+
return stitchedResult;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Create tiles for toolpath generation with overlap (using integer grid coordinates)
|
|
599
|
+
// toolWidth and toolHeight are in grid cells (not mm)
|
|
600
|
+
function createToolpathTiles(bounds, gridStep, xStep, yStep, toolWidthCells, toolHeightCells, maxMemoryBytes) {
|
|
601
|
+
// Calculate global grid dimensions
|
|
602
|
+
const globalGridWidth = Math.ceil((bounds.max.x - bounds.min.x) / gridStep) + 1;
|
|
603
|
+
const globalGridHeight = Math.ceil((bounds.max.y - bounds.min.y) / gridStep) + 1;
|
|
604
|
+
|
|
605
|
+
// Calculate tool overlap in grid cells (use radius = half diameter)
|
|
606
|
+
// Tool centered at tile boundary needs terrain extending half tool width beyond boundary
|
|
607
|
+
const toolOverlapX = Math.ceil(toolWidthCells / 2);
|
|
608
|
+
const toolOverlapY = Math.ceil(toolHeightCells / 2);
|
|
609
|
+
|
|
610
|
+
// Binary search for optimal tile size in grid cells
|
|
611
|
+
let low = Math.max(toolOverlapX, toolOverlapY) * 2; // At least 2x tool size
|
|
612
|
+
let high = Math.max(globalGridWidth, globalGridHeight);
|
|
613
|
+
let bestTileGridSize = high;
|
|
614
|
+
|
|
615
|
+
while (low <= high) {
|
|
616
|
+
const mid = Math.floor((low + high) / 2);
|
|
617
|
+
const outputW = Math.ceil(mid / xStep);
|
|
618
|
+
const outputH = Math.ceil(mid / yStep);
|
|
619
|
+
const memoryNeeded = outputW * outputH * 4;
|
|
620
|
+
|
|
621
|
+
if (memoryNeeded <= maxMemoryBytes) {
|
|
622
|
+
bestTileGridSize = mid;
|
|
623
|
+
low = mid + 1;
|
|
624
|
+
} else {
|
|
625
|
+
high = mid - 1;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const tilesX = Math.ceil(globalGridWidth / bestTileGridSize);
|
|
630
|
+
const tilesY = Math.ceil(globalGridHeight / bestTileGridSize);
|
|
631
|
+
const coreGridWidth = Math.ceil(globalGridWidth / tilesX);
|
|
632
|
+
const coreGridHeight = Math.ceil(globalGridHeight / tilesY);
|
|
633
|
+
|
|
634
|
+
// Calculate maximum tile dimensions (for buffer sizing)
|
|
635
|
+
const maxTileGridWidth = coreGridWidth + 2 * toolOverlapX;
|
|
636
|
+
const maxTileGridHeight = coreGridHeight + 2 * toolOverlapY;
|
|
637
|
+
|
|
638
|
+
debug.log(`Creating ${tilesX}×${tilesY} tiles (${coreGridWidth}×${coreGridHeight} cells core + ${toolOverlapX}×${toolOverlapY} cells overlap)`);
|
|
639
|
+
debug.log(`Max tile dimensions: ${maxTileGridWidth}×${maxTileGridHeight} cells (for buffer sizing)`);
|
|
640
|
+
|
|
641
|
+
const tiles = [];
|
|
642
|
+
for (let ty = 0; ty < tilesY; ty++) {
|
|
643
|
+
for (let tx = 0; tx < tilesX; tx++) {
|
|
644
|
+
// Core tile in grid coordinates
|
|
645
|
+
const coreGridStartX = tx * coreGridWidth;
|
|
646
|
+
const coreGridStartY = ty * coreGridHeight;
|
|
647
|
+
const coreGridEndX = Math.min((tx + 1) * coreGridWidth, globalGridWidth) - 1;
|
|
648
|
+
const coreGridEndY = Math.min((ty + 1) * coreGridHeight, globalGridHeight) - 1;
|
|
649
|
+
|
|
650
|
+
// Extended tile with overlap in grid coordinates
|
|
651
|
+
let extGridStartX = coreGridStartX;
|
|
652
|
+
let extGridStartY = coreGridStartY;
|
|
653
|
+
let extGridEndX = coreGridEndX;
|
|
654
|
+
let extGridEndY = coreGridEndY;
|
|
655
|
+
|
|
656
|
+
// Add overlap on sides that aren't at global boundary
|
|
657
|
+
if (tx > 0) extGridStartX -= toolOverlapX;
|
|
658
|
+
if (ty > 0) extGridStartY -= toolOverlapY;
|
|
659
|
+
if (tx < tilesX - 1) extGridEndX += toolOverlapX;
|
|
660
|
+
if (ty < tilesY - 1) extGridEndY += toolOverlapY;
|
|
661
|
+
|
|
662
|
+
// Clamp to global bounds
|
|
663
|
+
extGridStartX = Math.max(0, extGridStartX);
|
|
664
|
+
extGridStartY = Math.max(0, extGridStartY);
|
|
665
|
+
extGridEndX = Math.min(globalGridWidth - 1, extGridEndX);
|
|
666
|
+
extGridEndY = Math.min(globalGridHeight - 1, extGridEndY);
|
|
667
|
+
|
|
668
|
+
// Calculate actual dimensions for this tile
|
|
669
|
+
const tileGridWidth = extGridEndX - extGridStartX + 1;
|
|
670
|
+
const tileGridHeight = extGridEndY - extGridStartY + 1;
|
|
671
|
+
|
|
672
|
+
// Convert grid coordinates to world coordinates
|
|
673
|
+
const extMinX = bounds.min.x + extGridStartX * gridStep;
|
|
674
|
+
const extMinY = bounds.min.y + extGridStartY * gridStep;
|
|
675
|
+
const extMaxX = bounds.min.x + extGridEndX * gridStep;
|
|
676
|
+
const extMaxY = bounds.min.y + extGridEndY * gridStep;
|
|
677
|
+
|
|
678
|
+
const coreMinX = bounds.min.x + coreGridStartX * gridStep;
|
|
679
|
+
const coreMinY = bounds.min.y + coreGridStartY * gridStep;
|
|
680
|
+
const coreMaxX = bounds.min.x + coreGridEndX * gridStep;
|
|
681
|
+
const coreMaxY = bounds.min.y + coreGridEndY * gridStep;
|
|
682
|
+
|
|
683
|
+
tiles.push({
|
|
684
|
+
id: `tile_${tx}_${ty}`,
|
|
685
|
+
tx, ty,
|
|
686
|
+
tilesX, tilesY,
|
|
687
|
+
gridWidth: tileGridWidth,
|
|
688
|
+
gridHeight: tileGridHeight,
|
|
689
|
+
bounds: {
|
|
690
|
+
min: { x: extMinX, y: extMinY, z: bounds.min.z },
|
|
691
|
+
max: { x: extMaxX, y: extMaxY, z: bounds.max.z }
|
|
692
|
+
},
|
|
693
|
+
core: {
|
|
694
|
+
gridStart: { x: coreGridStartX, y: coreGridStartY },
|
|
695
|
+
gridEnd: { x: coreGridEndX, y: coreGridEndY },
|
|
696
|
+
min: { x: coreMinX, y: coreMinY },
|
|
697
|
+
max: { x: coreMaxX, y: coreMaxY }
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return { tiles, maxTileGridWidth, maxTileGridHeight };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Stitch toolpath tiles together, dropping overlap regions (using integer grid coordinates)
|
|
707
|
+
function stitchToolpathTiles(tileResults, globalBounds, gridStep, xStep, yStep) {
|
|
708
|
+
// Calculate global output dimensions
|
|
709
|
+
const globalWidth = Math.ceil((globalBounds.max.x - globalBounds.min.x) / gridStep) + 1;
|
|
710
|
+
const globalHeight = Math.ceil((globalBounds.max.y - globalBounds.min.y) / gridStep) + 1;
|
|
711
|
+
const globalPointsPerLine = Math.ceil(globalWidth / xStep);
|
|
712
|
+
const globalNumScanlines = Math.ceil(globalHeight / yStep);
|
|
713
|
+
|
|
714
|
+
debug.log(`Stitching toolpath: global grid ${globalWidth}x${globalHeight}, output ${globalPointsPerLine}x${globalNumScanlines}`);
|
|
715
|
+
|
|
716
|
+
const result = new Float32Array(globalPointsPerLine * globalNumScanlines);
|
|
717
|
+
result.fill(NaN);
|
|
718
|
+
|
|
719
|
+
// Fast path for 1x1 stepping: use bulk row copying
|
|
720
|
+
const use1x1FastPath = (xStep === 1 && yStep === 1);
|
|
721
|
+
|
|
722
|
+
for (const tileResult of tileResults) {
|
|
723
|
+
const tile = tileResult.tile;
|
|
724
|
+
const tileData = tileResult.pathData;
|
|
725
|
+
|
|
726
|
+
// Use the pre-calculated integer grid coordinates from tile.core
|
|
727
|
+
const coreGridStartX = tile.core.gridStart.x;
|
|
728
|
+
const coreGridStartY = tile.core.gridStart.y;
|
|
729
|
+
const coreGridEndX = tile.core.gridEnd.x;
|
|
730
|
+
const coreGridEndY = tile.core.gridEnd.y;
|
|
731
|
+
|
|
732
|
+
// Calculate tile's extended grid coordinates
|
|
733
|
+
const extGridStartX = Math.round((tile.bounds.min.x - globalBounds.min.x) / gridStep);
|
|
734
|
+
const extGridStartY = Math.round((tile.bounds.min.y - globalBounds.min.y) / gridStep);
|
|
735
|
+
|
|
736
|
+
let copiedCount = 0;
|
|
737
|
+
|
|
738
|
+
// Calculate output coordinate ranges for this tile's core
|
|
739
|
+
// Core region in grid coordinates
|
|
740
|
+
const coreGridWidth = coreGridEndX - coreGridStartX + 1;
|
|
741
|
+
const coreGridHeight = coreGridEndY - coreGridStartY + 1;
|
|
742
|
+
|
|
743
|
+
// Core region in output coordinates (sampled by xStep/yStep)
|
|
744
|
+
const coreOutStartX = Math.floor(coreGridStartX / xStep);
|
|
745
|
+
const coreOutStartY = Math.floor(coreGridStartY / yStep);
|
|
746
|
+
const coreOutEndX = Math.floor(coreGridEndX / xStep);
|
|
747
|
+
const coreOutEndY = Math.floor(coreGridEndY / yStep);
|
|
748
|
+
const coreOutWidth = coreOutEndX - coreOutStartX + 1;
|
|
749
|
+
const coreOutHeight = coreOutEndY - coreOutStartY + 1;
|
|
750
|
+
|
|
751
|
+
// Tile's extended region start in grid coordinates
|
|
752
|
+
const extOutStartX = Math.floor(extGridStartX / xStep);
|
|
753
|
+
const extOutStartY = Math.floor(extGridStartY / yStep);
|
|
754
|
+
|
|
755
|
+
// Copy entire rows at once (works for all stepping values)
|
|
756
|
+
for (let outY = 0; outY < coreOutHeight; outY++) {
|
|
757
|
+
const globalOutY = coreOutStartY + outY;
|
|
758
|
+
const tileOutY = globalOutY - extOutStartY;
|
|
759
|
+
|
|
760
|
+
if (globalOutY >= 0 && globalOutY < globalNumScanlines &&
|
|
761
|
+
tileOutY >= 0 && tileOutY < tileResult.numScanlines) {
|
|
762
|
+
|
|
763
|
+
const globalRowStart = globalOutY * globalPointsPerLine + coreOutStartX;
|
|
764
|
+
const tileRowStart = tileOutY * tileResult.pointsPerLine + (coreOutStartX - extOutStartX);
|
|
765
|
+
|
|
766
|
+
// Bulk copy entire row of output values
|
|
767
|
+
result.set(tileData.subarray(tileRowStart, tileRowStart + coreOutWidth), globalRowStart);
|
|
768
|
+
copiedCount += coreOutWidth;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
debug.log(` Tile ${tile.id}: copied ${copiedCount} values`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Count how many output values are still NaN (gaps)
|
|
776
|
+
let nanCount = 0;
|
|
777
|
+
for (let i = 0; i < result.length; i++) {
|
|
778
|
+
if (isNaN(result[i])) nanCount++;
|
|
779
|
+
}
|
|
780
|
+
debug.log(`Stitching complete: ${result.length} total values, ${nanCount} still NaN`);
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
pathData: result,
|
|
784
|
+
numScanlines: globalNumScanlines,
|
|
785
|
+
pointsPerLine: globalPointsPerLine,
|
|
786
|
+
generationTime: 0 // Sum from tiles if needed
|
|
787
|
+
};
|
|
788
|
+
}
|