@gridspace/raster-path 1.0.6 → 1.0.8

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.
@@ -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,12 @@ 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;
68
+ export let cachedRadialV3RotatePipeline = null;
69
+ export let cachedRadialV3RotateShaderModule = null;
70
+ export let cachedRadialV3BatchedRasterizePipeline = null;
71
+ export let cachedRadialV3BatchedRasterizeShaderModule = null;
66
72
 
67
73
  // Constants
68
74
  export const EMPTY_CELL = -1e10;
@@ -94,6 +100,9 @@ export function round(v, d = 1) {
94
100
  const rasterizeShaderCode = 'SHADER:planar-rasterize';
95
101
  const toolpathShaderCode = 'SHADER:planar-toolpath';
96
102
  const radialRasterizeShaderCode = 'SHADER:radial-raster';
103
+ const tracingShaderCode = 'SHADER:tracing-toolpath';
104
+ const radialV3RotateShaderCode = 'SHADER:radial-rotate-triangles';
105
+ const radialV3BatchedRasterizeShaderCode = 'SHADER:radial-rasterize-batched';
97
106
 
98
107
  // Initialize WebGPU device in worker context
99
108
  export async function initWebGPU() {
@@ -113,10 +122,7 @@ export async function initWebGPU() {
113
122
 
114
123
  // Request device with higher limits for large meshes
115
124
  const adapterLimits = adapter.limits;
116
- debug.log('Adapter limits:', {
117
- maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
118
- maxBufferSize: adapterLimits.maxBufferSize
119
- });
125
+ debug.log('Adapter limits:', adapterLimits);
120
126
 
121
127
  device = await adapter.requestDevice({
122
128
  requiredLimits: {
@@ -158,6 +164,33 @@ export async function initWebGPU() {
158
164
  compute: { module: cachedRadialBatchShaderModule, entryPoint: 'main' },
159
165
  });
160
166
 
167
+ // Pre-compile tracing shader module
168
+ cachedTracingShaderModule = device.createShaderModule({ code: tracingShaderCode });
169
+
170
+ // Pre-create tracing pipeline
171
+ cachedTracingPipeline = device.createComputePipeline({
172
+ layout: 'auto',
173
+ compute: { module: cachedTracingShaderModule, entryPoint: 'main' },
174
+ });
175
+
176
+ // Pre-compile radial V3 rotate shader module
177
+ cachedRadialV3RotateShaderModule = device.createShaderModule({ code: radialV3RotateShaderCode });
178
+
179
+ // Pre-create radial V3 rotate pipeline
180
+ cachedRadialV3RotatePipeline = device.createComputePipeline({
181
+ layout: 'auto',
182
+ compute: { module: cachedRadialV3RotateShaderModule, entryPoint: 'main' },
183
+ });
184
+
185
+ // Pre-compile radial V3 batched rasterize shader module
186
+ cachedRadialV3BatchedRasterizeShaderModule = device.createShaderModule({ code: radialV3BatchedRasterizeShaderCode });
187
+
188
+ // Pre-create radial V3 batched rasterize pipeline
189
+ cachedRadialV3BatchedRasterizePipeline = device.createComputePipeline({
190
+ layout: 'auto',
191
+ compute: { module: cachedRadialV3BatchedRasterizeShaderModule, entryPoint: 'main' },
192
+ });
193
+
161
194
  // Store device capabilities
162
195
  deviceCapabilities = {
163
196
  maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,