@gridspace/raster-path 1.0.7 → 1.0.9

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.
@@ -272,70 +272,162 @@ export async function generateTracingToolpaths({
272
272
  });
273
273
  device.queue.writeBuffer(maxZBuffer, 0, maxZInitData);
274
274
 
275
- // Process each path
276
- const outputPaths = [];
275
+ // ═══════════════════════════════════════════════════════════════════════
276
+ // PHASE 1: Sample all paths and build unified buffer
277
+ // ═══════════════════════════════════════════════════════════════════════
278
+ debug.log('PHASE 1: Sampling all paths...');
279
+ const pathIndex = []; // Maps path ID → unified buffer offsets
280
+ const sampledSegments = [];
277
281
  let totalSampledPoints = 0;
278
282
 
279
283
  for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
280
- const pathStartTime = performance.now();
281
284
  const inputPath = paths[pathIdx];
282
-
283
- debug.log(`Processing path ${pathIdx + 1}/${paths.length}: ${inputPath.length / 2} input vertices`);
285
+ debug.log(`Path ${pathIdx + 1}/${paths.length}: ${inputPath.length / 2} input vertices`);
284
286
 
285
287
  // Sample path at specified resolution
286
288
  const sampledPath = samplePath(inputPath, step);
287
- const numSampledPoints = sampledPath.length / 2;
288
- totalSampledPoints += numSampledPoints;
289
+ const numPoints = sampledPath.length / 2;
290
+
291
+ pathIndex.push({
292
+ startOffset: totalSampledPoints,
293
+ endOffset: totalSampledPoints + numPoints,
294
+ numPoints: numPoints
295
+ });
289
296
 
290
- debug.log(` Sampled to ${numSampledPoints} points`);
297
+ sampledSegments.push(sampledPath);
298
+ totalSampledPoints += numPoints;
291
299
 
292
- // Debug: Log first sampled point and its grid coordinates
293
- const firstX = sampledPath[0];
294
- const firstY = sampledPath[1];
300
+ debug.log(` Sampled to ${numPoints} points`);
301
+ }
302
+
303
+ // Concatenate all sampled paths into unified buffer
304
+ const unifiedSampledXY = new Float32Array(totalSampledPoints * 2);
305
+ let writeOffset = 0;
306
+
307
+ for (let pathIdx = 0; pathIdx < sampledSegments.length; pathIdx++) {
308
+ const sampledPath = sampledSegments[pathIdx];
309
+ unifiedSampledXY.set(sampledPath, writeOffset * 2);
310
+ writeOffset += sampledPath.length / 2;
311
+ }
312
+
313
+ debug.log(`Unified buffer: ${totalSampledPoints} total points from ${paths.length} paths`);
314
+
315
+ // Debug: Log first sampled point
316
+ if (totalSampledPoints > 0) {
317
+ const firstX = unifiedSampledXY[0];
318
+ const firstY = unifiedSampledXY[1];
295
319
  const gridX = (firstX - terrainBounds.min.x) / gridStep;
296
320
  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) {
321
+ debug.log(`First point: world(${firstX.toFixed(2)}, ${firstY.toFixed(2)}) -> grid(${gridX.toFixed(2)}, ${gridY.toFixed(2)})`);
322
+ }
323
+
324
+ // ═══════════════════════════════════════════════════════════════════════
325
+ // PHASE 2: Calculate memory budget and create chunks
326
+ // ═══════════════════════════════════════════════════════════════════════
327
+ debug.log('PHASE 2: Calculating memory budget and chunking...');
328
+ const bytesPerPoint = 8 + 4; // XY input (2 floats) + Z output (1 float)
329
+ const configuredLimit = config.maxGPUMemoryMB * 1024 * 1024;
330
+ const deviceLimit = deviceCapabilities.maxStorageBufferBindingSize;
331
+ const maxSafeSize = Math.min(configuredLimit, deviceLimit) * config.gpuMemorySafetyMargin;
332
+
333
+ // Fixed overhead: terrain, tool, maxZ, uniforms
334
+ const fixedOverhead = terrainPositions.byteLength +
335
+ (sparseToolData.count * 16) +
336
+ (paths.length * 4) +
337
+ 48;
338
+
339
+ if (fixedOverhead > maxSafeSize) {
340
+ if (shouldCleanupBuffers) {
309
341
  terrainBuffer.destroy();
310
342
  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
343
  }
344
+ throw new Error(
345
+ `Fixed buffers (terrain + tool) exceed GPU memory: ` +
346
+ `${(fixedOverhead / 1024 / 1024).toFixed(1)}MB > ` +
347
+ `${(maxSafeSize / 1024 / 1024).toFixed(1)}MB. ` +
348
+ `Try reducing terrain resolution or tool density.`
349
+ );
350
+ }
318
351
 
319
- // Create GPU buffers for this path
320
- const inputBuffer = device.createBuffer({
321
- size: sampledPath.byteLength,
322
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
352
+ const availableForPaths = maxSafeSize - fixedOverhead;
353
+ const maxPointsPerChunk = Math.floor(availableForPaths / bytesPerPoint);
354
+
355
+ debug.log(`Memory budget: ${(maxSafeSize / 1024 / 1024).toFixed(1)}MB safe, ${(availableForPaths / 1024 / 1024).toFixed(1)}MB available for paths`);
356
+ debug.log(`Max points per chunk: ${maxPointsPerChunk.toLocaleString()}`);
357
+
358
+ // Create chunks
359
+ const chunks = [];
360
+ let currentStart = 0;
361
+ while (currentStart < totalSampledPoints) {
362
+ const currentEnd = Math.min(currentStart + maxPointsPerChunk, totalSampledPoints);
363
+ chunks.push({
364
+ startPoint: currentStart,
365
+ endPoint: currentEnd,
366
+ numPoints: currentEnd - currentStart
323
367
  });
324
- device.queue.writeBuffer(inputBuffer, 0, sampledPath);
368
+ currentStart = currentEnd;
369
+ }
325
370
 
326
- const outputBuffer = device.createBuffer({
327
- size: outputBufferSize,
328
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
329
- });
371
+ debug.log(`Created ${chunks.length} chunk(s) for processing`);
372
+
373
+ // ═══════════════════════════════════════════════════════════════════════
374
+ // PHASE 3: Create reusable GPU buffers (buffer pool pattern)
375
+ // ═══════════════════════════════════════════════════════════════════════
376
+ debug.log('PHASE 3: Creating reusable GPU buffers...');
377
+
378
+ // Input buffer: XY pairs for sampled points
379
+ const inputBuffer = device.createBuffer({
380
+ size: maxPointsPerChunk * 8, // 2 floats per point
381
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
382
+ });
383
+
384
+ // Output buffer: Z depths
385
+ const outputBuffer = device.createBuffer({
386
+ size: maxPointsPerChunk * 4, // 1 float per point
387
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
388
+ });
389
+
390
+ // Uniform buffer
391
+ const uniformBuffer = device.createBuffer({
392
+ size: 48,
393
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
394
+ });
395
+
396
+ // Staging buffer for readback
397
+ const stagingBuffer = device.createBuffer({
398
+ size: maxPointsPerChunk * 4,
399
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
400
+ });
401
+
402
+ // Unified output array (filled chunk-by-chunk)
403
+ const unifiedOutputZ = new Float32Array(totalSampledPoints);
404
+
405
+ debug.log(`Buffers created for ${maxPointsPerChunk.toLocaleString()} points per chunk`);
330
406
 
331
- // Create uniforms (aligned to match shader struct)
332
- // Struct: 5 u32s + 4 f32s = 36 bytes, padded to 48 bytes for alignment
407
+ // ═══════════════════════════════════════════════════════════════════════
408
+ // PHASE 4: Process each chunk with single GPU dispatch
409
+ // ═══════════════════════════════════════════════════════════════════════
410
+ debug.log('PHASE 4: Processing chunks...');
411
+
412
+ for (let chunkIdx = 0; chunkIdx < chunks.length; chunkIdx++) {
413
+ const chunk = chunks[chunkIdx];
414
+ const { startPoint, endPoint, numPoints } = chunk;
415
+
416
+ debug.log(`Processing chunk ${chunkIdx + 1}/${chunks.length}: points ${startPoint}-${endPoint} (${numPoints} points)`);
417
+
418
+ // Extract chunk slice from unified buffer
419
+ const chunkInputXY = unifiedSampledXY.subarray(startPoint * 2, endPoint * 2);
420
+
421
+ // Upload to GPU (reuse same buffers)
422
+ device.queue.writeBuffer(inputBuffer, 0, chunkInputXY);
423
+
424
+ // Update uniforms for this chunk
333
425
  const uniformData = new Uint32Array(12); // 48 bytes
334
426
  uniformData[0] = terrainData.width;
335
427
  uniformData[1] = terrainData.height;
336
428
  uniformData[2] = sparseToolData.count;
337
- uniformData[3] = numSampledPoints;
338
- uniformData[4] = pathIdx; // path_index for maxZ buffer indexing
429
+ uniformData[3] = numPoints; // point_count for THIS CHUNK
430
+ uniformData[4] = 0; // path_index (unused, maxZ computed on CPU)
339
431
 
340
432
  const uniformDataFloat = new Float32Array(uniformData.buffer);
341
433
  uniformDataFloat[5] = terrainBounds.min.x;
@@ -343,16 +435,12 @@ export async function generateTracingToolpaths({
343
435
  uniformDataFloat[7] = gridStep;
344
436
  uniformDataFloat[8] = zFloor;
345
437
 
346
- const uniformBuffer = device.createBuffer({
347
- size: uniformData.byteLength,
348
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
349
- });
350
438
  device.queue.writeBuffer(uniformBuffer, 0, uniformData);
351
439
 
352
- // Wait for buffer uploads
440
+ // Wait for uploads
353
441
  await device.queue.onSubmittedWorkDone();
354
442
 
355
- // Create bind group
443
+ // Create bind group (same bindings as before)
356
444
  const bindGroup = device.createBindGroup({
357
445
  layout: cachedTracingPipeline.getBindGroupLayout(0),
358
446
  entries: [
@@ -360,28 +448,23 @@ export async function generateTracingToolpaths({
360
448
  { binding: 1, resource: { buffer: toolBuffer } },
361
449
  { binding: 2, resource: { buffer: inputBuffer } },
362
450
  { binding: 3, resource: { buffer: outputBuffer } },
363
- { binding: 4, resource: { buffer: maxZBuffer } },
451
+ { binding: 4, resource: { buffer: maxZBuffer } }, // Keep for shader compatibility
364
452
  { binding: 5, resource: { buffer: uniformBuffer } },
365
453
  ],
366
454
  });
367
455
 
368
- // Dispatch compute shader
456
+ // Single GPU dispatch for entire chunk
369
457
  const commandEncoder = device.createCommandEncoder();
370
458
  const passEncoder = commandEncoder.beginComputePass();
371
459
  passEncoder.setPipeline(cachedTracingPipeline);
372
460
  passEncoder.setBindGroup(0, bindGroup);
373
461
 
374
- const workgroupsX = Math.ceil(numSampledPoints / 64);
462
+ const workgroupsX = Math.ceil(numPoints / 64);
375
463
  passEncoder.dispatchWorkgroups(workgroupsX);
376
464
  passEncoder.end();
377
465
 
378
466
  // 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);
467
+ commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, numPoints * 4);
385
468
  device.queue.submit([commandEncoder.finish()]);
386
469
 
387
470
  // Wait for GPU to finish
@@ -389,65 +472,79 @@ export async function generateTracingToolpaths({
389
472
 
390
473
  // Read back results
391
474
  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
- }
475
+ const chunkOutputZ = new Float32Array(stagingBuffer.getMappedRange(), 0, numPoints);
403
476
 
404
- outputPaths.push(outputXYZ);
477
+ // Copy to unified output array
478
+ unifiedOutputZ.set(chunkOutputZ, startPoint);
405
479
 
406
- // Cleanup path-specific buffers
407
- inputBuffer.destroy();
408
- outputBuffer.destroy();
409
- uniformBuffer.destroy();
410
- stagingBuffer.destroy();
480
+ stagingBuffer.unmap();
411
481
 
412
- const pathTime = performance.now() - pathStartTime;
413
- debug.log(` Path ${pathIdx + 1} complete: ${numSampledPoints} points in ${pathTime.toFixed(1)}ms`);
482
+ debug.log(` Chunk ${chunkIdx + 1} complete: ${numPoints} points processed`);
414
483
 
415
- // Report progress
484
+ // Report progress (point-based, not path-based)
416
485
  if (onProgress) {
417
486
  onProgress({
418
487
  type: 'tracing-progress',
419
488
  data: {
420
- percent: Math.round(((pathIdx + 1) / paths.length) * 100),
421
- current: pathIdx + 1,
422
- total: paths.length,
423
- pathIndex: pathIdx
489
+ percent: Math.round((endPoint / totalSampledPoints) * 100),
490
+ current: endPoint,
491
+ total: totalSampledPoints,
492
+ chunkIndex: chunkIdx + 1,
493
+ totalChunks: chunks.length
424
494
  }
425
495
  });
426
496
  }
427
497
  }
428
498
 
429
- // Read back maxZ buffer
430
- const maxZStagingBuffer = device.createBuffer({
431
- size: maxZInitData.byteLength,
432
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
433
- });
499
+ // Cleanup reusable buffers
500
+ inputBuffer.destroy();
501
+ outputBuffer.destroy();
502
+ uniformBuffer.destroy();
503
+ stagingBuffer.destroy();
504
+
505
+ debug.log('All chunks processed');
506
+
507
+ // ═══════════════════════════════════════════════════════════════════════
508
+ // PHASE 5: Remap unified output back to individual paths & compute maxZ
509
+ // ═══════════════════════════════════════════════════════════════════════
510
+ debug.log('PHASE 5: Remapping to individual paths and computing maxZ...');
511
+
512
+ const outputPaths = [];
513
+ const maxZValues = new Array(paths.length).fill(zFloor);
514
+
515
+ for (let pathIdx = 0; pathIdx < pathIndex.length; pathIdx++) {
516
+ const { startOffset, numPoints } = pathIndex[pathIdx];
517
+
518
+ if (numPoints === 0) {
519
+ outputPaths.push(new Float32Array(0)); // Empty path
520
+ debug.log(`Path ${pathIdx + 1}: empty`);
521
+ continue;
522
+ }
523
+
524
+ // Allocate XYZ output
525
+ const pathXYZ = new Float32Array(numPoints * 3);
434
526
 
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();
527
+ // Copy from unified buffers + compute maxZ
528
+ for (let i = 0; i < numPoints; i++) {
529
+ const unifiedIdx = startOffset + i;
530
+ const x = unifiedSampledXY[unifiedIdx * 2 + 0];
531
+ const y = unifiedSampledXY[unifiedIdx * 2 + 1];
532
+ const z = unifiedOutputZ[unifiedIdx];
439
533
 
440
- await maxZStagingBuffer.mapAsync(GPUMapMode.READ);
441
- const maxZBitsI32 = new Int32Array(maxZStagingBuffer.getMappedRange());
442
- const maxZBitsCopy = new Int32Array(maxZBitsI32);
443
- maxZStagingBuffer.unmap();
534
+ pathXYZ[i * 3 + 0] = x;
535
+ pathXYZ[i * 3 + 1] = y;
536
+ pathXYZ[i * 3 + 2] = z;
444
537
 
445
- // Convert i32 bits back to f32 values
446
- const maxZValues = new Float32Array(maxZBitsCopy.buffer);
538
+ // Track max Z for this path (CPU-side)
539
+ maxZValues[pathIdx] = Math.max(maxZValues[pathIdx], z);
540
+ }
447
541
 
448
- // Cleanup buffers
542
+ outputPaths.push(pathXYZ);
543
+ debug.log(`Path ${pathIdx + 1}: ${numPoints} points, maxZ=${maxZValues[pathIdx].toFixed(2)}`);
544
+ }
545
+
546
+ // Cleanup maxZ buffer (was only used for shader compatibility)
449
547
  maxZBuffer.destroy();
450
- maxZStagingBuffer.destroy();
451
548
 
452
549
  // Cleanup temporary buffers only (don't destroy cached buffers)
453
550
  if (shouldCleanupBuffers) {
@@ -468,25 +565,19 @@ export async function generateTracingToolpaths({
468
565
  }
469
566
 
470
567
  /**
471
- * TODO: Batched path processing
568
+ * IMPLEMENTATION NOTE: Unified Batching System
472
569
  *
473
- * OPTIMIZATION OPPORTUNITY:
474
- * Currently processes one path at a time. For better GPU utilization:
570
+ * This function uses a unified batching approach for optimal performance:
475
571
  *
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
572
+ * 1. All paths are sampled and concatenated into a single unified buffer
573
+ * 2. Paths are chunked based on GPU memory limits (handles giant paths)
574
+ * 3. Each chunk is processed with a single GPU dispatch (reduces overhead)
575
+ * 4. Output is remapped back to individual path arrays
576
+ * 5. MaxZ is computed on CPU (avoids complex GPU atomic coordination)
480
577
  *
481
578
  * 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
579
+ * - Handles paths that exceed GPU memory limits (automatic chunking)
580
+ * - Reduces GPU dispatch overhead (10-100x for many small paths)
581
+ * - Better progress tracking (point-based instead of path-based)
582
+ * - Buffer pool pattern reduces allocation overhead
492
583
  */
@@ -65,6 +65,10 @@ export let cachedRadialBatchPipeline = null;
65
65
  export let cachedRadialBatchShaderModule = null;
66
66
  export let cachedTracingPipeline = null;
67
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;
68
72
 
69
73
  // Constants
70
74
  export const EMPTY_CELL = -1e10;
@@ -97,6 +101,8 @@ const rasterizeShaderCode = 'SHADER:planar-rasterize';
97
101
  const toolpathShaderCode = 'SHADER:planar-toolpath';
98
102
  const radialRasterizeShaderCode = 'SHADER:radial-raster';
99
103
  const tracingShaderCode = 'SHADER:tracing-toolpath';
104
+ const radialV3RotateShaderCode = 'SHADER:radial-rotate-triangles';
105
+ const radialV3BatchedRasterizeShaderCode = 'SHADER:radial-rasterize-batched';
100
106
 
101
107
  // Initialize WebGPU device in worker context
102
108
  export async function initWebGPU() {
@@ -167,6 +173,24 @@ export async function initWebGPU() {
167
173
  compute: { module: cachedTracingShaderModule, entryPoint: 'main' },
168
174
  });
169
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
+
170
194
  // Store device capabilities
171
195
  deviceCapabilities = {
172
196
  maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
@@ -111,6 +111,8 @@ export class RasterPath {
111
111
  gpuMemorySafetyMargin: config.gpuMemorySafetyMargin ?? 0.8,
112
112
  autoTiling: config.autoTiling ?? true,
113
113
  batchDivisor: config.batchDivisor ?? 1, // For testing batching overhead
114
+ radialV3: config.radialV3 ?? false, // Use radial V3 pipeline (rotate-filter-toolpath)
115
+ radialV4: config.radialV4 ?? false, // Use radial V4 pipeline (slice-based lathe)
114
116
  debug: config.debug,
115
117
  quiet: config.quiet
116
118
  };
@@ -531,9 +533,15 @@ export class RasterPath {
531
533
  resolve(data);
532
534
  };
533
535
 
534
- // Send entire pipeline to worker
536
+ // Send entire pipeline to worker (use V3 or V4 if configured)
537
+ const messageType = this.config.radialV4
538
+ ? 'radial-generate-toolpaths-v4'
539
+ : this.config.radialV3
540
+ ? 'radial-generate-toolpaths-v3'
541
+ : 'radial-generate-toolpaths';
542
+
535
543
  this.#sendMessage(
536
- 'radial-generate-toolpaths',
544
+ messageType,
537
545
  {
538
546
  triangles: triangles,
539
547
  bucketData,
@@ -54,6 +54,7 @@ import { initWebGPU, setConfig, updateConfig, deviceCapabilities, debug, device
54
54
  import { rasterizeMesh } from './raster-planar.js';
55
55
  import { generateToolpath } from './path-planar.js';
56
56
  import { generateRadialToolpaths } from './path-radial.js';
57
+ import { generateRadialToolpathsV3 } from './path-radial-v3.js';
57
58
  import { generateTracingToolpaths, createReusableTracingBuffers, destroyReusableTracingBuffers } from './path-tracing.js';
58
59
  import { calibrateGPU } from './workload-calibrate.js';
59
60
 
@@ -128,6 +129,15 @@ self.onmessage = async function(e) {
128
129
  }, toolpathTransferBuffers);
129
130
  break;
130
131
 
132
+ case 'radial-generate-toolpaths-v3':
133
+ const radialV3ToolpathResult = await generateRadialToolpathsV3(data);
134
+ const v3ToolpathTransferBuffers = radialV3ToolpathResult.strips.map(strip => strip.pathData.buffer);
135
+ self.postMessage({
136
+ type: 'radial-toolpaths-complete',
137
+ data: radialV3ToolpathResult
138
+ }, v3ToolpathTransferBuffers);
139
+ break;
140
+
131
141
  case 'tracing-generate-toolpaths':
132
142
  const tracingResult = await generateTracingToolpaths({
133
143
  paths: data.paths,