@gridspace/raster-path 1.0.6 → 1.0.7
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/app.js +151 -12
- package/build/index.html +6 -2
- package/build/raster-path.js +118 -9
- package/build/raster-worker.js +421 -7
- package/package.json +4 -2
- package/src/core/path-radial.js +0 -1
- package/src/core/path-tracing.js +492 -0
- package/src/core/raster-config.js +13 -4
- package/src/core/raster-path.js +118 -9
- package/src/core/raster-worker.js +41 -0
- package/src/core/workload-calibrate.js +57 -3
- package/src/shaders/tracing-toolpath.wgsl +95 -0
- package/src/test/tracing-test.cjs +307 -0
- package/src/web/app.js +151 -12
- package/src/web/index.html +6 -2
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
* Path Tracing - Toolpath Z-Depth Tracing for Input Polylines
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
*
|
|
6
|
+
* Generates toolpath Z-coordinates by following input polylines and calculating
|
|
7
|
+
* tool contact depth at each sampled point along the path.
|
|
8
|
+
*
|
|
9
|
+
* EXPORTS:
|
|
10
|
+
* ────────
|
|
11
|
+
* Functions:
|
|
12
|
+
* - generateTracingToolpaths(options)
|
|
13
|
+
* Main API - processes array of input paths, returns array of XYZ paths
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* ──────────
|
|
17
|
+
* For each input polyline:
|
|
18
|
+
* 1. Sample path segments at 'step' resolution (densification)
|
|
19
|
+
* 2. For each sampled point (X, Y):
|
|
20
|
+
* - Convert world coordinates to terrain grid coordinates
|
|
21
|
+
* - Test tool collision with terrain at that position
|
|
22
|
+
* - Calculate maximum collision Z (same algorithm as planar mode)
|
|
23
|
+
* 3. Build output array with X, Y, Z triplets
|
|
24
|
+
*
|
|
25
|
+
* PATH SAMPLING:
|
|
26
|
+
* ──────────────
|
|
27
|
+
* Input paths are arrays of XY coordinate pairs. The 'step' parameter controls
|
|
28
|
+
* how densely segments are sampled:
|
|
29
|
+
* - Vertices are always included
|
|
30
|
+
* - Segments longer than 'step' are subdivided
|
|
31
|
+
* - Output maintains original vertex positions + interpolated points
|
|
32
|
+
*
|
|
33
|
+
* OUTPUT FORMAT:
|
|
34
|
+
* ──────────────
|
|
35
|
+
* Array of Float32Array buffers, each containing XYZ triplets:
|
|
36
|
+
* [x1, y1, z1, x2, y2, z2, x3, y3, z3, ...]
|
|
37
|
+
*
|
|
38
|
+
* MEMORY SAFETY:
|
|
39
|
+
* ──────────────
|
|
40
|
+
* Validates that sampled path points will fit in GPU buffers before processing.
|
|
41
|
+
* Throws error if estimated memory exceeds safe limits.
|
|
42
|
+
*
|
|
43
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
device, deviceCapabilities, isInitialized, config,
|
|
48
|
+
cachedTracingPipeline, debug, initWebGPU
|
|
49
|
+
} from './raster-config.js';
|
|
50
|
+
import { createSparseToolFromPoints } from './raster-tool.js';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reusable GPU buffers for iterative tracing
|
|
54
|
+
* Stored globally in worker to be reused across multiple generateTracingToolpaths calls
|
|
55
|
+
*/
|
|
56
|
+
let cachedTracingBuffers = null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create reusable GPU buffers for tracing (terrain and tool buffers)
|
|
60
|
+
* These persist across multiple generateTracingToolpaths calls
|
|
61
|
+
* @param {Float32Array} terrainPositions - Dense terrain Z-only grid
|
|
62
|
+
* @param {Float32Array} toolPositions - Tool points (XYZ triplets)
|
|
63
|
+
* @returns {Object} - Buffer handles and metadata
|
|
64
|
+
*/
|
|
65
|
+
export function createReusableTracingBuffers(terrainPositions, toolPositions) {
|
|
66
|
+
if (!isInitialized) {
|
|
67
|
+
throw new Error('WebGPU not initialized');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Destroy existing buffers if any
|
|
71
|
+
if (cachedTracingBuffers) {
|
|
72
|
+
destroyReusableTracingBuffers();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create sparse tool representation
|
|
76
|
+
const sparseToolData = createSparseToolFromPoints(toolPositions);
|
|
77
|
+
debug.log(`Created reusable tracing buffers: terrain ${terrainPositions.length} floats, tool ${sparseToolData.count} points`);
|
|
78
|
+
|
|
79
|
+
// Create terrain buffer
|
|
80
|
+
const terrainBuffer = device.createBuffer({
|
|
81
|
+
size: terrainPositions.byteLength,
|
|
82
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
83
|
+
});
|
|
84
|
+
device.queue.writeBuffer(terrainBuffer, 0, terrainPositions);
|
|
85
|
+
|
|
86
|
+
// Create tool buffer
|
|
87
|
+
const toolBufferData = new ArrayBuffer(sparseToolData.count * 16);
|
|
88
|
+
const toolBufferI32 = new Int32Array(toolBufferData);
|
|
89
|
+
const toolBufferF32 = new Float32Array(toolBufferData);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < sparseToolData.count; i++) {
|
|
92
|
+
toolBufferI32[i * 4 + 0] = sparseToolData.xOffsets[i];
|
|
93
|
+
toolBufferI32[i * 4 + 1] = sparseToolData.yOffsets[i];
|
|
94
|
+
toolBufferF32[i * 4 + 2] = sparseToolData.zValues[i];
|
|
95
|
+
toolBufferF32[i * 4 + 3] = 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const toolBuffer = device.createBuffer({
|
|
99
|
+
size: toolBufferData.byteLength,
|
|
100
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
101
|
+
});
|
|
102
|
+
device.queue.writeBuffer(toolBuffer, 0, toolBufferData);
|
|
103
|
+
|
|
104
|
+
cachedTracingBuffers = {
|
|
105
|
+
terrainBuffer,
|
|
106
|
+
toolBuffer,
|
|
107
|
+
sparseToolData
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return cachedTracingBuffers;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Destroy reusable tracing buffers
|
|
115
|
+
*/
|
|
116
|
+
export function destroyReusableTracingBuffers() {
|
|
117
|
+
if (cachedTracingBuffers) {
|
|
118
|
+
cachedTracingBuffers.terrainBuffer.destroy();
|
|
119
|
+
cachedTracingBuffers.toolBuffer.destroy();
|
|
120
|
+
cachedTracingBuffers = null;
|
|
121
|
+
debug.log('Destroyed reusable tracing buffers');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sample a path at specified step resolution
|
|
127
|
+
* @param {Float32Array} pathXY - Input path as XY coordinate pairs
|
|
128
|
+
* @param {number} step - Maximum distance between sampled points (world units)
|
|
129
|
+
* @returns {Float32Array} - Sampled XY coordinates
|
|
130
|
+
*/
|
|
131
|
+
function samplePath(pathXY, step) {
|
|
132
|
+
if (pathXY.length < 2) {
|
|
133
|
+
// Empty or single-point path
|
|
134
|
+
return new Float32Array(pathXY);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const numVertices = pathXY.length / 2;
|
|
138
|
+
const sampledPoints = [];
|
|
139
|
+
|
|
140
|
+
// Always include first vertex
|
|
141
|
+
sampledPoints.push(pathXY[0], pathXY[1]);
|
|
142
|
+
|
|
143
|
+
// Process each segment
|
|
144
|
+
for (let i = 0; i < numVertices - 1; i++) {
|
|
145
|
+
const x1 = pathXY[i * 2];
|
|
146
|
+
const y1 = pathXY[i * 2 + 1];
|
|
147
|
+
const x2 = pathXY[(i + 1) * 2];
|
|
148
|
+
const y2 = pathXY[(i + 1) * 2 + 1];
|
|
149
|
+
|
|
150
|
+
const dx = x2 - x1;
|
|
151
|
+
const dy = y2 - y1;
|
|
152
|
+
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
153
|
+
|
|
154
|
+
// If segment is longer than step, subdivide it
|
|
155
|
+
if (segmentLength > step) {
|
|
156
|
+
const numSubdivisions = Math.ceil(segmentLength / step);
|
|
157
|
+
const subdivisionStep = 1.0 / numSubdivisions;
|
|
158
|
+
|
|
159
|
+
// Add interpolated points (skip t=0 since it's already added, skip t=1 since it's the next vertex)
|
|
160
|
+
for (let j = 1; j < numSubdivisions; j++) {
|
|
161
|
+
const t = j * subdivisionStep;
|
|
162
|
+
const x = x1 + t * dx;
|
|
163
|
+
const y = y1 + t * dy;
|
|
164
|
+
sampledPoints.push(x, y);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Add next vertex (except for last iteration where it's already the end)
|
|
169
|
+
if (i < numVertices - 1) {
|
|
170
|
+
sampledPoints.push(x2, y2);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return new Float32Array(sampledPoints);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generate tracing toolpaths for input polylines
|
|
179
|
+
* @param {Object} options - Configuration
|
|
180
|
+
* @param {Float32Array[]} options.paths - Array of input paths (XY coordinate pairs)
|
|
181
|
+
* @param {Float32Array} options.terrainPositions - Dense terrain Z-only grid
|
|
182
|
+
* @param {Object} options.terrainData - Terrain metadata (width, height, bounds)
|
|
183
|
+
* @param {Float32Array} options.toolPositions - Tool points (XYZ triplets)
|
|
184
|
+
* @param {number} options.step - Sampling resolution along paths (world units)
|
|
185
|
+
* @param {number} options.gridStep - Terrain rasterization resolution
|
|
186
|
+
* @param {Object} options.terrainBounds - Terrain bounding box
|
|
187
|
+
* @param {number} options.zFloor - Minimum Z depth for out-of-bounds points
|
|
188
|
+
* @param {Function} options.onProgress - Progress callback
|
|
189
|
+
* @returns {Promise<Object>} - Result with array of XYZ paths
|
|
190
|
+
*/
|
|
191
|
+
export async function generateTracingToolpaths({
|
|
192
|
+
paths,
|
|
193
|
+
terrainPositions,
|
|
194
|
+
terrainData,
|
|
195
|
+
toolPositions,
|
|
196
|
+
step,
|
|
197
|
+
gridStep,
|
|
198
|
+
terrainBounds,
|
|
199
|
+
zFloor,
|
|
200
|
+
onProgress
|
|
201
|
+
}) {
|
|
202
|
+
const startTime = performance.now();
|
|
203
|
+
debug.log('Generating tracing toolpaths...');
|
|
204
|
+
debug.log(`Input: ${paths.length} paths, step=${step}, gridStep=${gridStep}, zFloor=${zFloor}`);
|
|
205
|
+
debug.log(`Terrain: ${terrainData.width}×${terrainData.height}, bounds: min(${terrainBounds.min.x.toFixed(2)}, ${terrainBounds.min.y.toFixed(2)}) max(${terrainBounds.max.x.toFixed(2)}, ${terrainBounds.max.y.toFixed(2)})`);
|
|
206
|
+
|
|
207
|
+
// Initialize WebGPU if needed
|
|
208
|
+
if (!isInitialized) {
|
|
209
|
+
const success = await initWebGPU();
|
|
210
|
+
if (!success) {
|
|
211
|
+
throw new Error('WebGPU not available');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Use cached buffers if available, otherwise create them for this call
|
|
216
|
+
let terrainBuffer, toolBuffer, sparseToolData;
|
|
217
|
+
let shouldCleanupBuffers = false;
|
|
218
|
+
|
|
219
|
+
if (cachedTracingBuffers) {
|
|
220
|
+
// Reuse existing buffers (optimized for iterative tracing)
|
|
221
|
+
debug.log('Using cached tracing buffers');
|
|
222
|
+
terrainBuffer = cachedTracingBuffers.terrainBuffer;
|
|
223
|
+
toolBuffer = cachedTracingBuffers.toolBuffer;
|
|
224
|
+
sparseToolData = cachedTracingBuffers.sparseToolData;
|
|
225
|
+
} else {
|
|
226
|
+
// Create temporary buffers (will be cleaned up at end)
|
|
227
|
+
debug.log('Creating temporary tracing buffers');
|
|
228
|
+
sparseToolData = createSparseToolFromPoints(toolPositions);
|
|
229
|
+
debug.log(`Created sparse tool: ${sparseToolData.count} points`);
|
|
230
|
+
|
|
231
|
+
// Create terrain buffer
|
|
232
|
+
terrainBuffer = device.createBuffer({
|
|
233
|
+
size: terrainPositions.byteLength,
|
|
234
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
235
|
+
});
|
|
236
|
+
device.queue.writeBuffer(terrainBuffer, 0, terrainPositions);
|
|
237
|
+
|
|
238
|
+
// Create tool buffer
|
|
239
|
+
const toolBufferData = new ArrayBuffer(sparseToolData.count * 16);
|
|
240
|
+
const toolBufferI32 = new Int32Array(toolBufferData);
|
|
241
|
+
const toolBufferF32 = new Float32Array(toolBufferData);
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < sparseToolData.count; i++) {
|
|
244
|
+
toolBufferI32[i * 4 + 0] = sparseToolData.xOffsets[i];
|
|
245
|
+
toolBufferI32[i * 4 + 1] = sparseToolData.yOffsets[i];
|
|
246
|
+
toolBufferF32[i * 4 + 2] = sparseToolData.zValues[i];
|
|
247
|
+
toolBufferF32[i * 4 + 3] = 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
toolBuffer = device.createBuffer({
|
|
251
|
+
size: toolBufferData.byteLength,
|
|
252
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
253
|
+
});
|
|
254
|
+
device.queue.writeBuffer(toolBuffer, 0, toolBufferData);
|
|
255
|
+
|
|
256
|
+
// Wait for buffer uploads to complete
|
|
257
|
+
await device.queue.onSubmittedWorkDone();
|
|
258
|
+
|
|
259
|
+
shouldCleanupBuffers = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Create maxZ buffer (one i32 per path for atomic operations)
|
|
263
|
+
// Initialize to sentinel value (bitcast of -1e30)
|
|
264
|
+
const SENTINEL_Z = -1e30;
|
|
265
|
+
const sentinelBits = new Float32Array([SENTINEL_Z]);
|
|
266
|
+
const sentinelI32 = new Int32Array(sentinelBits.buffer)[0];
|
|
267
|
+
const maxZInitData = new Int32Array(paths.length).fill(sentinelI32);
|
|
268
|
+
|
|
269
|
+
const maxZBuffer = device.createBuffer({
|
|
270
|
+
size: maxZInitData.byteLength,
|
|
271
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
272
|
+
});
|
|
273
|
+
device.queue.writeBuffer(maxZBuffer, 0, maxZInitData);
|
|
274
|
+
|
|
275
|
+
// Process each path
|
|
276
|
+
const outputPaths = [];
|
|
277
|
+
let totalSampledPoints = 0;
|
|
278
|
+
|
|
279
|
+
for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
|
|
280
|
+
const pathStartTime = performance.now();
|
|
281
|
+
const inputPath = paths[pathIdx];
|
|
282
|
+
|
|
283
|
+
debug.log(`Processing path ${pathIdx + 1}/${paths.length}: ${inputPath.length / 2} input vertices`);
|
|
284
|
+
|
|
285
|
+
// Sample path at specified resolution
|
|
286
|
+
const sampledPath = samplePath(inputPath, step);
|
|
287
|
+
const numSampledPoints = sampledPath.length / 2;
|
|
288
|
+
totalSampledPoints += numSampledPoints;
|
|
289
|
+
|
|
290
|
+
debug.log(` Sampled to ${numSampledPoints} points`);
|
|
291
|
+
|
|
292
|
+
// Debug: Log first sampled point and its grid coordinates
|
|
293
|
+
const firstX = sampledPath[0];
|
|
294
|
+
const firstY = sampledPath[1];
|
|
295
|
+
const gridX = (firstX - terrainBounds.min.x) / gridStep;
|
|
296
|
+
const gridY = (firstY - terrainBounds.min.y) / gridStep;
|
|
297
|
+
debug.log(` First point: world(${firstX.toFixed(2)}, ${firstY.toFixed(2)}) -> grid(${gridX.toFixed(2)}, ${gridY.toFixed(2)})`);
|
|
298
|
+
debug.log(` Terrain: ${terrainData.width}x${terrainData.height}, bounds: (${terrainBounds.min.x.toFixed(2)}, ${terrainBounds.min.y.toFixed(2)}) to (${terrainBounds.max.x.toFixed(2)}, ${terrainBounds.max.y.toFixed(2)})`);
|
|
299
|
+
|
|
300
|
+
// Check GPU memory limits
|
|
301
|
+
const inputBufferSize = sampledPath.byteLength;
|
|
302
|
+
const outputBufferSize = numSampledPoints * 4; // 4 bytes per float (Z only)
|
|
303
|
+
const estimatedMemory = inputBufferSize + outputBufferSize;
|
|
304
|
+
const configuredLimit = config.maxGPUMemoryMB * 1024 * 1024;
|
|
305
|
+
const deviceLimit = deviceCapabilities.maxStorageBufferBindingSize;
|
|
306
|
+
const maxSafeSize = Math.min(configuredLimit, deviceLimit) * config.gpuMemorySafetyMargin;
|
|
307
|
+
|
|
308
|
+
if (estimatedMemory > maxSafeSize) {
|
|
309
|
+
terrainBuffer.destroy();
|
|
310
|
+
toolBuffer.destroy();
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Path ${pathIdx + 1} exceeds GPU memory limits: ` +
|
|
313
|
+
`${(estimatedMemory / 1024 / 1024).toFixed(1)}MB > ` +
|
|
314
|
+
`${(maxSafeSize / 1024 / 1024).toFixed(1)}MB safe limit. ` +
|
|
315
|
+
`Consider reducing step parameter or splitting path.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Create GPU buffers for this path
|
|
320
|
+
const inputBuffer = device.createBuffer({
|
|
321
|
+
size: sampledPath.byteLength,
|
|
322
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
323
|
+
});
|
|
324
|
+
device.queue.writeBuffer(inputBuffer, 0, sampledPath);
|
|
325
|
+
|
|
326
|
+
const outputBuffer = device.createBuffer({
|
|
327
|
+
size: outputBufferSize,
|
|
328
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Create uniforms (aligned to match shader struct)
|
|
332
|
+
// Struct: 5 u32s + 4 f32s = 36 bytes, padded to 48 bytes for alignment
|
|
333
|
+
const uniformData = new Uint32Array(12); // 48 bytes
|
|
334
|
+
uniformData[0] = terrainData.width;
|
|
335
|
+
uniformData[1] = terrainData.height;
|
|
336
|
+
uniformData[2] = sparseToolData.count;
|
|
337
|
+
uniformData[3] = numSampledPoints;
|
|
338
|
+
uniformData[4] = pathIdx; // path_index for maxZ buffer indexing
|
|
339
|
+
|
|
340
|
+
const uniformDataFloat = new Float32Array(uniformData.buffer);
|
|
341
|
+
uniformDataFloat[5] = terrainBounds.min.x;
|
|
342
|
+
uniformDataFloat[6] = terrainBounds.min.y;
|
|
343
|
+
uniformDataFloat[7] = gridStep;
|
|
344
|
+
uniformDataFloat[8] = zFloor;
|
|
345
|
+
|
|
346
|
+
const uniformBuffer = device.createBuffer({
|
|
347
|
+
size: uniformData.byteLength,
|
|
348
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
349
|
+
});
|
|
350
|
+
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
|
|
351
|
+
|
|
352
|
+
// Wait for buffer uploads
|
|
353
|
+
await device.queue.onSubmittedWorkDone();
|
|
354
|
+
|
|
355
|
+
// Create bind group
|
|
356
|
+
const bindGroup = device.createBindGroup({
|
|
357
|
+
layout: cachedTracingPipeline.getBindGroupLayout(0),
|
|
358
|
+
entries: [
|
|
359
|
+
{ binding: 0, resource: { buffer: terrainBuffer } },
|
|
360
|
+
{ binding: 1, resource: { buffer: toolBuffer } },
|
|
361
|
+
{ binding: 2, resource: { buffer: inputBuffer } },
|
|
362
|
+
{ binding: 3, resource: { buffer: outputBuffer } },
|
|
363
|
+
{ binding: 4, resource: { buffer: maxZBuffer } },
|
|
364
|
+
{ binding: 5, resource: { buffer: uniformBuffer } },
|
|
365
|
+
],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Dispatch compute shader
|
|
369
|
+
const commandEncoder = device.createCommandEncoder();
|
|
370
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
371
|
+
passEncoder.setPipeline(cachedTracingPipeline);
|
|
372
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
373
|
+
|
|
374
|
+
const workgroupsX = Math.ceil(numSampledPoints / 64);
|
|
375
|
+
passEncoder.dispatchWorkgroups(workgroupsX);
|
|
376
|
+
passEncoder.end();
|
|
377
|
+
|
|
378
|
+
// Copy output to staging buffer
|
|
379
|
+
const stagingBuffer = device.createBuffer({
|
|
380
|
+
size: outputBufferSize,
|
|
381
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputBufferSize);
|
|
385
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
386
|
+
|
|
387
|
+
// Wait for GPU to finish
|
|
388
|
+
await device.queue.onSubmittedWorkDone();
|
|
389
|
+
|
|
390
|
+
// Read back results
|
|
391
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
392
|
+
const outputDepths = new Float32Array(stagingBuffer.getMappedRange());
|
|
393
|
+
const depthsCopy = new Float32Array(outputDepths);
|
|
394
|
+
stagingBuffer.unmap();
|
|
395
|
+
|
|
396
|
+
// Build XYZ output array
|
|
397
|
+
const outputXYZ = new Float32Array(numSampledPoints * 3);
|
|
398
|
+
for (let i = 0; i < numSampledPoints; i++) {
|
|
399
|
+
outputXYZ[i * 3 + 0] = sampledPath[i * 2 + 0]; // X
|
|
400
|
+
outputXYZ[i * 3 + 1] = sampledPath[i * 2 + 1]; // Y
|
|
401
|
+
outputXYZ[i * 3 + 2] = depthsCopy[i]; // Z
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
outputPaths.push(outputXYZ);
|
|
405
|
+
|
|
406
|
+
// Cleanup path-specific buffers
|
|
407
|
+
inputBuffer.destroy();
|
|
408
|
+
outputBuffer.destroy();
|
|
409
|
+
uniformBuffer.destroy();
|
|
410
|
+
stagingBuffer.destroy();
|
|
411
|
+
|
|
412
|
+
const pathTime = performance.now() - pathStartTime;
|
|
413
|
+
debug.log(` Path ${pathIdx + 1} complete: ${numSampledPoints} points in ${pathTime.toFixed(1)}ms`);
|
|
414
|
+
|
|
415
|
+
// Report progress
|
|
416
|
+
if (onProgress) {
|
|
417
|
+
onProgress({
|
|
418
|
+
type: 'tracing-progress',
|
|
419
|
+
data: {
|
|
420
|
+
percent: Math.round(((pathIdx + 1) / paths.length) * 100),
|
|
421
|
+
current: pathIdx + 1,
|
|
422
|
+
total: paths.length,
|
|
423
|
+
pathIndex: pathIdx
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Read back maxZ buffer
|
|
430
|
+
const maxZStagingBuffer = device.createBuffer({
|
|
431
|
+
size: maxZInitData.byteLength,
|
|
432
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const maxZCommandEncoder = device.createCommandEncoder();
|
|
436
|
+
maxZCommandEncoder.copyBufferToBuffer(maxZBuffer, 0, maxZStagingBuffer, 0, maxZInitData.byteLength);
|
|
437
|
+
device.queue.submit([maxZCommandEncoder.finish()]);
|
|
438
|
+
await device.queue.onSubmittedWorkDone();
|
|
439
|
+
|
|
440
|
+
await maxZStagingBuffer.mapAsync(GPUMapMode.READ);
|
|
441
|
+
const maxZBitsI32 = new Int32Array(maxZStagingBuffer.getMappedRange());
|
|
442
|
+
const maxZBitsCopy = new Int32Array(maxZBitsI32);
|
|
443
|
+
maxZStagingBuffer.unmap();
|
|
444
|
+
|
|
445
|
+
// Convert i32 bits back to f32 values
|
|
446
|
+
const maxZValues = new Float32Array(maxZBitsCopy.buffer);
|
|
447
|
+
|
|
448
|
+
// Cleanup buffers
|
|
449
|
+
maxZBuffer.destroy();
|
|
450
|
+
maxZStagingBuffer.destroy();
|
|
451
|
+
|
|
452
|
+
// Cleanup temporary buffers only (don't destroy cached buffers)
|
|
453
|
+
if (shouldCleanupBuffers) {
|
|
454
|
+
terrainBuffer.destroy();
|
|
455
|
+
toolBuffer.destroy();
|
|
456
|
+
debug.log('Cleaned up temporary tracing buffers');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const endTime = performance.now();
|
|
460
|
+
debug.log(`Tracing complete: ${paths.length} paths, ${totalSampledPoints} total points in ${(endTime - startTime).toFixed(1)}ms`);
|
|
461
|
+
debug.log(`Max Z values: [${Array.from(maxZValues).map(z => z.toFixed(2)).join(', ')}]`);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
paths: outputPaths,
|
|
465
|
+
maxZ: Array.from(maxZValues),
|
|
466
|
+
generationTime: endTime - startTime
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* TODO: Batched path processing
|
|
472
|
+
*
|
|
473
|
+
* OPTIMIZATION OPPORTUNITY:
|
|
474
|
+
* Currently processes one path at a time. For better GPU utilization:
|
|
475
|
+
*
|
|
476
|
+
* 1. Concatenate all sampled paths into single input buffer
|
|
477
|
+
* 2. Create offset table: [path1Start, path1End, path2Start, path2End, ...]
|
|
478
|
+
* 3. Single GPU dispatch processes all paths
|
|
479
|
+
* 4. Split output buffer back into individual path arrays
|
|
480
|
+
*
|
|
481
|
+
* BENEFITS:
|
|
482
|
+
* - Reduce GPU dispatch overhead (N dispatches → 1 dispatch)
|
|
483
|
+
* - Better GPU occupancy (more threads active)
|
|
484
|
+
* - Fewer buffer create/destroy cycles
|
|
485
|
+
*
|
|
486
|
+
* COMPLEXITY:
|
|
487
|
+
* - Need offset management in shader or CPU-side splitting
|
|
488
|
+
* - Memory limit checking becomes more complex
|
|
489
|
+
* - Progress reporting granularity reduced (can still report workgroup completion)
|
|
490
|
+
*
|
|
491
|
+
* ESTIMATE: 2-5x speedup for many small paths, minimal benefit for few large paths
|
|
492
|
+
*/
|
|
@@ -63,6 +63,8 @@ export let cachedToolpathPipeline = null;
|
|
|
63
63
|
export let cachedToolpathShaderModule = null;
|
|
64
64
|
export let cachedRadialBatchPipeline = null;
|
|
65
65
|
export let cachedRadialBatchShaderModule = null;
|
|
66
|
+
export let cachedTracingPipeline = null;
|
|
67
|
+
export let cachedTracingShaderModule = null;
|
|
66
68
|
|
|
67
69
|
// Constants
|
|
68
70
|
export const EMPTY_CELL = -1e10;
|
|
@@ -94,6 +96,7 @@ export function round(v, d = 1) {
|
|
|
94
96
|
const rasterizeShaderCode = 'SHADER:planar-rasterize';
|
|
95
97
|
const toolpathShaderCode = 'SHADER:planar-toolpath';
|
|
96
98
|
const radialRasterizeShaderCode = 'SHADER:radial-raster';
|
|
99
|
+
const tracingShaderCode = 'SHADER:tracing-toolpath';
|
|
97
100
|
|
|
98
101
|
// Initialize WebGPU device in worker context
|
|
99
102
|
export async function initWebGPU() {
|
|
@@ -113,10 +116,7 @@ export async function initWebGPU() {
|
|
|
113
116
|
|
|
114
117
|
// Request device with higher limits for large meshes
|
|
115
118
|
const adapterLimits = adapter.limits;
|
|
116
|
-
debug.log('Adapter limits:',
|
|
117
|
-
maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
|
|
118
|
-
maxBufferSize: adapterLimits.maxBufferSize
|
|
119
|
-
});
|
|
119
|
+
debug.log('Adapter limits:', adapterLimits);
|
|
120
120
|
|
|
121
121
|
device = await adapter.requestDevice({
|
|
122
122
|
requiredLimits: {
|
|
@@ -158,6 +158,15 @@ export async function initWebGPU() {
|
|
|
158
158
|
compute: { module: cachedRadialBatchShaderModule, entryPoint: 'main' },
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
// Pre-compile tracing shader module
|
|
162
|
+
cachedTracingShaderModule = device.createShaderModule({ code: tracingShaderCode });
|
|
163
|
+
|
|
164
|
+
// Pre-create tracing pipeline
|
|
165
|
+
cachedTracingPipeline = device.createComputePipeline({
|
|
166
|
+
layout: 'auto',
|
|
167
|
+
compute: { module: cachedTracingShaderModule, entryPoint: 'main' },
|
|
168
|
+
});
|
|
169
|
+
|
|
161
170
|
// Store device capabilities
|
|
162
171
|
deviceCapabilities = {
|
|
163
172
|
maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
|