@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.
package/build/app.js CHANGED
@@ -12,6 +12,7 @@ let zFloor = -100;
12
12
  let xStep = 5;
13
13
  let yStep = 5;
14
14
  let angleStep = 1.0; // degrees
15
+ let traceStep = 0.5; // mm - sampling resolution for tracing mode
15
16
  let toolSize = 2.5; // mm - target tool diameter
16
17
 
17
18
  let modelSTL = null; // ArrayBuffer (current, possibly rotated)
@@ -61,6 +62,7 @@ function saveParameters() {
61
62
  localStorage.setItem('raster-xStep', xStep);
62
63
  localStorage.setItem('raster-yStep', yStep);
63
64
  localStorage.setItem('raster-angleStep', angleStep);
65
+ localStorage.setItem('raster-traceStep', traceStep);
64
66
  localStorage.setItem('raster-toolSize', toolSize);
65
67
  console.log(`[App] Saved tool size: ${toolSize}mm`);
66
68
 
@@ -69,6 +71,12 @@ function saveParameters() {
69
71
  if (showWrappedCheckbox) {
70
72
  localStorage.setItem('raster-showWrapped', showWrappedCheckbox.checked);
71
73
  }
74
+
75
+ // Save radial V3 checkbox
76
+ const radialV3Checkbox = document.getElementById('radial-v3');
77
+ if (radialV3Checkbox) {
78
+ localStorage.setItem('raster-radialV3', radialV3Checkbox.checked);
79
+ }
72
80
  }
73
81
 
74
82
  function loadParameters() {
@@ -127,6 +135,12 @@ function loadParameters() {
127
135
  document.getElementById('angle-step').value = angleStep;
128
136
  }
129
137
 
138
+ const savedTraceStep = localStorage.getItem('raster-traceStep');
139
+ if (savedTraceStep !== null) {
140
+ traceStep = parseFloat(savedTraceStep);
141
+ document.getElementById('trace-step').value = traceStep;
142
+ }
143
+
130
144
  const savedToolSize = localStorage.getItem('raster-toolSize');
131
145
  if (savedToolSize !== null) {
132
146
  toolSize = parseFloat(savedToolSize);
@@ -145,6 +159,15 @@ function loadParameters() {
145
159
  showWrappedCheckbox.checked = savedShowWrapped === 'true';
146
160
  }
147
161
  }
162
+
163
+ // Restore radial V3 checkbox
164
+ const savedRadialV3 = localStorage.getItem('raster-radialV3');
165
+ if (savedRadialV3 !== null) {
166
+ const radialV3Checkbox = document.getElementById('radial-v3');
167
+ if (radialV3Checkbox) {
168
+ radialV3Checkbox.checked = savedRadialV3 === 'true';
169
+ }
170
+ }
148
171
  }
149
172
 
150
173
  // ============================================================================
@@ -513,10 +536,14 @@ async function initRasterPath() {
513
536
  rasterPath.terminate();
514
537
  }
515
538
 
539
+ const radialV3Checkbox = document.getElementById('radial-v3');
540
+ const useRadialV3 = mode === 'radial' && radialV3Checkbox && radialV3Checkbox.checked;
541
+
516
542
  rasterPath = new RasterPath({
517
543
  mode: mode,
518
544
  resolution: resolution,
519
545
  rotationStep: mode === 'radial' ? angleStep : undefined,
546
+ radialV3: useRadialV3,
520
547
  batchDivisor: 5,
521
548
  debug: true
522
549
  });
@@ -545,8 +572,8 @@ async function rasterizeAll() {
545
572
  updateInfo(`Tool loaded in ${(t1 - t0).toFixed(0)}ms`);
546
573
  }
547
574
 
548
- if (mode === 'planar') {
549
- // Planar mode: rasterize terrain immediately
575
+ if (mode === 'planar' || mode === 'tracing') {
576
+ // Planar/Tracing mode: rasterize terrain immediately
550
577
  if (modelTriangles) {
551
578
  updateInfo('Rasterizing terrain...');
552
579
  const t0 = performance.now();
@@ -557,7 +584,7 @@ async function rasterizeAll() {
557
584
  const t1 = performance.now();
558
585
  updateInfo(`Terrain rasterized in ${(t1 - t0).toFixed(0)}ms`);
559
586
  }
560
- } else {
587
+ } else if (mode === 'radial') {
561
588
  // Radial mode: MUST load tool FIRST
562
589
  if (!toolTriangles) {
563
590
  updateInfo('Error: Radial mode requires tool to be loaded first');
@@ -608,30 +635,81 @@ async function generateToolpath() {
608
635
  updateInfo('Model must be rasterized first');
609
636
  return;
610
637
  }
611
- } else {
638
+ } else if (mode === 'radial') {
612
639
  // Radial mode: terrain must be loaded (stored internally)
613
640
  if (!modelTriangles) {
614
641
  updateInfo('Model STL must be loaded');
615
642
  return;
616
643
  }
644
+ } else if (mode === 'tracing') {
645
+ // Tracing mode: terrain must be rasterized
646
+ if (!modelRasterData) {
647
+ updateInfo('Model must be rasterized first');
648
+ return;
649
+ }
617
650
  }
618
651
 
619
652
  try {
620
653
  const t0 = performance.now();
621
654
  updateInfo('Generating toolpath...');
622
655
 
623
- // Unified API - works for both modes!
624
- toolpathData = await rasterPath.generateToolpaths({
625
- xStep: xStep,
626
- yStep: yStep,
656
+ // Generate trace paths for tracing mode
657
+ let tracePaths = null;
658
+ if (mode === 'tracing') {
659
+ // Get model bounds from raster data
660
+ const bounds = modelRasterData.bounds;
661
+ const minX = bounds.min.x;
662
+ const maxX = bounds.max.x;
663
+ const minY = bounds.min.y;
664
+ const maxY = bounds.max.y;
665
+
666
+ // Create two cross paths: horizontal through center, vertical through center
667
+ const centerY = (minY + maxY) / 2;
668
+ const centerX = (minX + maxX) / 2;
669
+
670
+ tracePaths = [
671
+ new Float32Array([minX, centerY, maxX, centerY]), // Horizontal line
672
+ new Float32Array([centerX, minY, centerX, maxY]) // Vertical line
673
+ ];
674
+
675
+ debug.log(`Generated trace paths: H(${minX.toFixed(2)}, ${centerY.toFixed(2)}) to (${maxX.toFixed(2)}, ${centerY.toFixed(2)})`);
676
+ debug.log(` V(${centerX.toFixed(2)}, ${minY.toFixed(2)}) to (${centerX.toFixed(2)}, ${maxY.toFixed(2)})`);
677
+ }
678
+
679
+ // Unified API - works for all modes!
680
+ const generateParams = {
627
681
  zFloor: zFloor
628
- });
682
+ };
683
+
684
+ if (mode === 'tracing') {
685
+ generateParams.paths = tracePaths;
686
+ generateParams.step = traceStep;
687
+ } else {
688
+ generateParams.xStep = xStep;
689
+ generateParams.yStep = yStep;
690
+ }
691
+
692
+ toolpathData = await rasterPath.generateToolpaths(generateParams);
629
693
 
630
694
  const t1 = performance.now();
631
695
 
632
696
  if (mode === 'planar') {
633
697
  const numPoints = toolpathData.pathData.length;
634
698
  updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
699
+ } else if (mode === 'tracing') {
700
+ const totalPoints = toolpathData.paths.reduce((sum, path) => sum + path.length / 3, 0);
701
+ debug.log(`[Tracing] Generated ${toolpathData.paths.length} paths with ${totalPoints} total points`);
702
+
703
+ // Log sample Z values from each path
704
+ toolpathData.paths.forEach((path, idx) => {
705
+ const zValues = [];
706
+ for (let i = 2; i < Math.min(path.length, 15); i += 3) {
707
+ zValues.push(path[i].toFixed(2));
708
+ }
709
+ debug.log(`[Tracing] Path ${idx} Z samples:`, zValues.join(', '));
710
+ });
711
+
712
+ updateInfo(`Toolpath generated: ${toolpathData.paths.length} paths, ${totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
635
713
  } else {
636
714
  // debug.log('[Radial] Toolpaths generated:', toolpathData);
637
715
  debug.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
@@ -901,8 +979,8 @@ function displayModelRaster(wrapped) {
901
979
  const positions = [];
902
980
  const colors = [];
903
981
 
904
- if (mode === 'planar') {
905
- // Planar: terrain is dense (Z-only array)
982
+ if (mode === 'planar' || mode === 'tracing') {
983
+ // Planar/Tracing: terrain is dense (Z-only array)
906
984
  const { positions: rasterPos, bounds, gridWidth, gridHeight } = modelRasterData;
907
985
  const stepSize = resolution;
908
986
 
@@ -1057,6 +1135,59 @@ function displayToolpaths(wrapped) {
1057
1135
  return;
1058
1136
  }
1059
1137
 
1138
+ if (mode === 'tracing') {
1139
+ // Tracing toolpaths - array of XYZ paths
1140
+ const { paths } = toolpathData;
1141
+
1142
+ // Calculate total points
1143
+ let totalPoints = 0;
1144
+ for (const path of paths) {
1145
+ totalPoints += path.length / 3;
1146
+ }
1147
+
1148
+ debug.log('[Toolpath Display] Tracing mode:', paths.length, 'paths,', totalPoints, 'total points');
1149
+
1150
+ // Preallocate typed arrays
1151
+ const positions = new Float32Array(totalPoints * 3);
1152
+ const colors = new Float32Array(totalPoints * 3);
1153
+
1154
+ let arrayIdx = 0;
1155
+ for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
1156
+ const path = paths[pathIdx];
1157
+ const numPoints = path.length / 3;
1158
+
1159
+ // Use different colors for each path
1160
+ const color = pathIdx === 0 ? [1, 0.4, 0] : [0, 0.8, 1]; // Orange for horizontal, cyan for vertical
1161
+
1162
+ for (let i = 0; i < numPoints; i++) {
1163
+ positions[arrayIdx] = path[i * 3]; // X
1164
+ positions[arrayIdx + 1] = path[i * 3 + 1]; // Y
1165
+ positions[arrayIdx + 2] = path[i * 3 + 2]; // Z
1166
+
1167
+ colors[arrayIdx] = color[0];
1168
+ colors[arrayIdx + 1] = color[1];
1169
+ colors[arrayIdx + 2] = color[2];
1170
+
1171
+ arrayIdx += 3;
1172
+ }
1173
+ }
1174
+
1175
+ // Create geometry
1176
+ const geometry = new THREE.BufferGeometry();
1177
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1178
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
1179
+
1180
+ const material = new THREE.PointsMaterial({
1181
+ size: resolution * 1.5,
1182
+ vertexColors: true
1183
+ });
1184
+
1185
+ toolpathPoints = new THREE.Points(geometry, material);
1186
+ rotatedGroup.add(toolpathPoints);
1187
+
1188
+ return; // Exit early for tracing mode
1189
+ }
1190
+
1060
1191
  if (mode === 'planar') {
1061
1192
  // Planar toolpaths
1062
1193
  const { pathData, numScanlines, pointsPerLine } = toolpathData;
@@ -1324,15 +1455,35 @@ function updateButtonStates() {
1324
1455
  // ============================================================================
1325
1456
 
1326
1457
  function updateModeUI() {
1327
- // Show/hide wrapped toggle and angle step for radial mode
1458
+ // Show/hide mode-specific controls
1328
1459
  const wrappedContainer = document.getElementById('wrapped-container').classList;
1329
1460
  const angleStepContainer = document.getElementById('angle-step-container').classList;
1461
+ const traceStepContainer = document.getElementById('trace-step-container').classList;
1462
+ const xStepContainer = document.getElementById('x-step-container').classList;
1463
+ const yStepContainer = document.getElementById('y-step-container').classList;
1464
+ const radialV3Container = document.getElementById('radial-v3-container').classList;
1465
+
1330
1466
  if (mode === 'radial') {
1331
1467
  wrappedContainer.remove('hide');
1332
1468
  angleStepContainer.remove('hide');
1469
+ traceStepContainer.add('hide');
1470
+ xStepContainer.remove('hide');
1471
+ yStepContainer.remove('hide');
1472
+ radialV3Container.remove('hide');
1473
+ } else if (mode === 'tracing') {
1474
+ wrappedContainer.add('hide');
1475
+ angleStepContainer.add('hide');
1476
+ traceStepContainer.remove('hide');
1477
+ xStepContainer.add('hide');
1478
+ yStepContainer.add('hide');
1333
1479
  } else {
1480
+ // planar
1334
1481
  wrappedContainer.add('hide');
1335
1482
  angleStepContainer.add('hide');
1483
+ traceStepContainer.add('hide');
1484
+ radialV3Container.add('hide');
1485
+ xStepContainer.remove('hide');
1486
+ yStepContainer.remove('hide');
1336
1487
  }
1337
1488
  }
1338
1489
 
@@ -1446,12 +1597,36 @@ document.addEventListener('DOMContentLoaded', async () => {
1446
1597
  modelRasterData = null; // Need to re-rasterize with new angle step
1447
1598
  toolRasterData = null;
1448
1599
  toolpathData = null;
1600
+ initRasterPath(); // Reinit with new angle step
1449
1601
  }
1450
1602
  saveParameters();
1451
1603
  updateInfo(`Angle Step changed to ${angleStep}°`);
1452
1604
  updateButtonStates();
1453
1605
  });
1454
1606
 
1607
+ document.getElementById('trace-step').addEventListener('change', (e) => {
1608
+ traceStep = parseFloat(e.target.value);
1609
+ if (mode === 'tracing') {
1610
+ toolpathData = null; // Need to regenerate toolpath
1611
+ }
1612
+ saveParameters();
1613
+ updateInfo(`Trace Step changed to ${traceStep}mm`);
1614
+ updateButtonStates();
1615
+ });
1616
+
1617
+ document.getElementById('radial-v3').addEventListener('change', (e) => {
1618
+ if (mode === 'radial') {
1619
+ modelRasterData = null; // Need to re-rasterize with different algorithm
1620
+ toolRasterData = null;
1621
+ toolpathData = null;
1622
+ initRasterPath(); // Reinit with V3 setting
1623
+ }
1624
+ saveParameters();
1625
+ const v3Status = e.target.checked ? 'V3 (experimental)' : 'V2 (default)';
1626
+ updateInfo(`Radial algorithm: ${v3Status}`);
1627
+ updateButtonStates();
1628
+ });
1629
+
1455
1630
  // Tool size change
1456
1631
  document.getElementById('tool-size').addEventListener('change', async (e) => {
1457
1632
  toolSize = parseFloat(e.target.value);
package/build/index.html CHANGED
@@ -53,6 +53,7 @@
53
53
  <div class="mode-toggle">
54
54
  <label><input type="radio" name="mode" value="planar" checked> Planar</label>
55
55
  <label><input type="radio" name="mode" value="radial"> Radial</label>
56
+ <label><input type="radio" name="mode" value="tracing"> Tracing</label>
56
57
  </div>
57
58
  </div>
58
59
 
@@ -79,15 +80,21 @@
79
80
  <label>
80
81
  Z Floor: <input type="number" id="z-floor" value="-100" step="10" style="width: 70px;">
81
82
  </label>
82
- <label>
83
+ <label id="x-step-container">
83
84
  X Step: <input type="number" id="x-step" value="5" min="1" max="50" style="width: 60px;">
84
85
  </label>
85
- <label>
86
+ <label id="y-step-container">
86
87
  Y Step: <input type="number" id="y-step" value="5" min="1" max="50" style="width: 60px;">
87
88
  </label>
88
89
  <label id="angle-step-container" class="hide">
89
90
  Angle Step (deg): <input type="number" id="angle-step" value="1" min="0.1" max="10" step="0.1" style="width: 60px;">
90
91
  </label>
92
+ <label id="trace-step-container" class="hide">
93
+ Trace Step (mm): <input type="number" id="trace-step" value="0.5" min="0.1" max="5" step="0.1" style="width: 60px;">
94
+ </label>
95
+ <label id="radial-v3-container" class="hide">
96
+ <input type="checkbox" id="radial-v3"> Use V3 (experimental)
97
+ </label>
91
98
  </div>
92
99
 
93
100
  <div class="section">
@@ -45,7 +45,7 @@
45
45
  /**
46
46
  * Configuration options for RasterPath
47
47
  * @typedef {Object} RasterPathConfig
48
- * @property {'planar'|'radial'} mode - Rasterization mode (default: 'planar')
48
+ * @property {'planar'|'radial'|'tracing'} mode - Rasterization mode (default: 'planar')
49
49
  * @property {boolean} autoTiling - Automatically tile large datasets (default: true)
50
50
  * @property {number} gpuMemorySafetyMargin - Safety margin as percentage (default: 0.8 = 80%)
51
51
  * @property {number} maxGPUMemoryMB - Maximum GPU memory per tile (default: 256MB)
@@ -80,8 +80,8 @@ export class RasterPath {
80
80
 
81
81
  // Validate mode
82
82
  const mode = config.mode || 'planar';
83
- if (mode !== 'planar' && mode !== 'radial') {
84
- throw new Error(`Invalid mode: ${mode}. Must be 'planar' or 'radial'`);
83
+ if (mode !== 'planar' && mode !== 'radial' && mode !== 'tracing') {
84
+ throw new Error(`Invalid mode: ${mode}. Must be 'planar', 'radial', or 'tracing'`);
85
85
  }
86
86
 
87
87
  // Validate rotationStep for radial mode
@@ -111,6 +111,7 @@ 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)
114
115
  debug: config.debug,
115
116
  quiet: config.quiet
116
117
  };
@@ -221,8 +222,8 @@ export class RasterPath {
221
222
  throw new Error('RasterPath not initialized. Call init() first.');
222
223
  }
223
224
 
224
- if (this.mode === 'planar') {
225
- // Planar: rasterize and return
225
+ if (this.mode === 'planar' || this.mode === 'tracing') {
226
+ // Planar/Tracing: rasterize and return (tracing reuses planar terrain rasterization)
226
227
  const terrainData = await this.#rasterizePlanar({ triangles, zFloor, boundsOverride, isForTool: false, onProgress });
227
228
  this.terrainData = terrainData;
228
229
  return terrainData;
@@ -269,7 +270,7 @@ export class RasterPath {
269
270
  * @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
270
271
  * @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
271
272
  */
272
- async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
273
+ async generateToolpaths({ xStep, yStep, zFloor, onProgress, paths, step }) {
273
274
  if (!this.isInitialized) {
274
275
  throw new Error('RasterPath not initialized. Call init() first.');
275
276
  }
@@ -278,7 +279,7 @@ export class RasterPath {
278
279
  throw new Error('Tool not loaded. Call loadTool() first.');
279
280
  }
280
281
 
281
- debug.log('gen.paths', { xStep, yStep, zFloor });
282
+ debug.log('gen.paths', { xStep, yStep, zFloor, paths: paths?.length, step });
282
283
 
283
284
  if (this.mode === 'planar') {
284
285
  if (!this.terrainData) {
@@ -292,7 +293,7 @@ export class RasterPath {
292
293
  zFloor,
293
294
  onProgress
294
295
  });
295
- } else {
296
+ } else if (this.mode === 'radial') {
296
297
  // Radial mode: use stored triangles
297
298
  if (!this.terrainTriangles) {
298
299
  throw new Error('Terrain not loaded. Call loadTerrain() first.');
@@ -306,14 +307,83 @@ export class RasterPath {
306
307
  zFloor: zFloor ?? this.terrainZFloor,
307
308
  onProgress
308
309
  });
310
+ } else if (this.mode === 'tracing') {
311
+ // Tracing mode: follow input paths
312
+ if (!this.terrainData) {
313
+ throw new Error('Terrain not loaded. Call loadTerrain() first.');
314
+ }
315
+ if (!paths || paths.length === 0) {
316
+ throw new Error('Tracing mode requires paths parameter (array of Float32Array XY coordinates)');
317
+ }
318
+ if (!step || step <= 0) {
319
+ throw new Error('Tracing mode requires step parameter (sampling resolution in world units)');
320
+ }
321
+ return this.#generateToolpathsTracing({
322
+ paths,
323
+ step,
324
+ zFloor,
325
+ onProgress
326
+ });
309
327
  }
310
328
  }
311
329
 
330
+ /**
331
+ * Create reusable GPU buffers for tracing mode (optimization for iterative tracing)
332
+ * Call this after loadTerrain() and loadTool() to cache buffers across multiple trace calls
333
+ * @returns {Promise<void>}
334
+ */
335
+ async createTracingBuffers() {
336
+ if (this.mode !== 'tracing') {
337
+ throw new Error('createTracingBuffers() only available in tracing mode');
338
+ }
339
+ if (!this.terrainData || !this.toolData) {
340
+ throw new Error('Must call loadTerrain() and loadTool() before createTracingBuffers()');
341
+ }
342
+
343
+ return new Promise((resolve, reject) => {
344
+ const handler = () => resolve();
345
+ this.#sendMessage(
346
+ 'create-tracing-buffers',
347
+ {
348
+ terrainPositions: this.terrainData.positions,
349
+ toolPositions: this.toolData.positions
350
+ },
351
+ 'tracing-buffers-created',
352
+ handler
353
+ );
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Destroy reusable tracing buffers
359
+ * @returns {Promise<void>}
360
+ */
361
+ async destroyTracingBuffers() {
362
+ if (this.mode !== 'tracing') {
363
+ return; // No-op for non-tracing modes
364
+ }
365
+
366
+ return new Promise((resolve, reject) => {
367
+ const handler = () => resolve();
368
+ this.#sendMessage(
369
+ 'destroy-tracing-buffers',
370
+ {},
371
+ 'tracing-buffers-destroyed',
372
+ handler
373
+ );
374
+ });
375
+ }
376
+
312
377
  /**
313
378
  * Terminate worker and cleanup resources
314
379
  */
315
- terminate() {
380
+ async terminate() {
316
381
  if (this.worker) {
382
+ // Cleanup tracing buffers if in tracing mode
383
+ if (this.mode === 'tracing') {
384
+ await this.destroyTracingBuffers();
385
+ }
386
+
317
387
  this.worker.terminate();
318
388
  this.worker = null;
319
389
  this.isInitialized = false;
@@ -388,6 +458,46 @@ export class RasterPath {
388
458
  });
389
459
  }
390
460
 
461
+ async #generateToolpathsTracing({ paths, step, zFloor, onProgress }) {
462
+ return new Promise((resolve, reject) => {
463
+ // Set up progress handler if callback provided
464
+ if (onProgress) {
465
+ const progressHandler = (data) => {
466
+ onProgress(data.percent, { current: data.current, total: data.total, pathIndex: data.pathIndex });
467
+ };
468
+ this.messageHandlers.set('tracing-progress', progressHandler);
469
+ }
470
+
471
+ const handler = (data) => {
472
+ // Clean up progress handler
473
+ if (onProgress) {
474
+ this.messageHandlers.delete('tracing-progress');
475
+ }
476
+ resolve(data);
477
+ };
478
+
479
+ this.#sendMessage(
480
+ 'tracing-generate-toolpaths',
481
+ {
482
+ paths,
483
+ terrainPositions: this.terrainData.positions,
484
+ terrainData: {
485
+ width: this.terrainData.gridWidth,
486
+ height: this.terrainData.gridHeight,
487
+ bounds: this.terrainData.bounds
488
+ },
489
+ toolPositions: this.toolData.positions,
490
+ step,
491
+ gridStep: this.resolution,
492
+ terrainBounds: this.terrainData.bounds,
493
+ zFloor: zFloor ?? 0
494
+ },
495
+ 'tracing-toolpaths-complete',
496
+ handler
497
+ );
498
+ });
499
+ }
500
+
391
501
  async #generateToolpathsRadial({ triangles, bounds, toolData, xStep, yStep, zFloor, onProgress }) {
392
502
  const maxRadius = this.#calculateMaxRadius(triangles);
393
503
 
@@ -422,9 +532,13 @@ export class RasterPath {
422
532
  resolve(data);
423
533
  };
424
534
 
425
- // Send entire pipeline to worker
535
+ // Send entire pipeline to worker (use V3 if configured)
536
+ const messageType = this.config.radialV3
537
+ ? 'radial-generate-toolpaths-v3'
538
+ : 'radial-generate-toolpaths';
539
+
426
540
  this.#sendMessage(
427
- 'radial-generate-toolpaths',
541
+ messageType,
428
542
  {
429
543
  triangles: triangles,
430
544
  bucketData,