@gridspace/raster-path 1.0.5 → 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 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
 
@@ -127,6 +129,12 @@ function loadParameters() {
127
129
  document.getElementById('angle-step').value = angleStep;
128
130
  }
129
131
 
132
+ const savedTraceStep = localStorage.getItem('raster-traceStep');
133
+ if (savedTraceStep !== null) {
134
+ traceStep = parseFloat(savedTraceStep);
135
+ document.getElementById('trace-step').value = traceStep;
136
+ }
137
+
130
138
  const savedToolSize = localStorage.getItem('raster-toolSize');
131
139
  if (savedToolSize !== null) {
132
140
  toolSize = parseFloat(savedToolSize);
@@ -545,8 +553,8 @@ async function rasterizeAll() {
545
553
  updateInfo(`Tool loaded in ${(t1 - t0).toFixed(0)}ms`);
546
554
  }
547
555
 
548
- if (mode === 'planar') {
549
- // Planar mode: rasterize terrain immediately
556
+ if (mode === 'planar' || mode === 'tracing') {
557
+ // Planar/Tracing mode: rasterize terrain immediately
550
558
  if (modelTriangles) {
551
559
  updateInfo('Rasterizing terrain...');
552
560
  const t0 = performance.now();
@@ -557,7 +565,7 @@ async function rasterizeAll() {
557
565
  const t1 = performance.now();
558
566
  updateInfo(`Terrain rasterized in ${(t1 - t0).toFixed(0)}ms`);
559
567
  }
560
- } else {
568
+ } else if (mode === 'radial') {
561
569
  // Radial mode: MUST load tool FIRST
562
570
  if (!toolTriangles) {
563
571
  updateInfo('Error: Radial mode requires tool to be loaded first');
@@ -608,30 +616,81 @@ async function generateToolpath() {
608
616
  updateInfo('Model must be rasterized first');
609
617
  return;
610
618
  }
611
- } else {
619
+ } else if (mode === 'radial') {
612
620
  // Radial mode: terrain must be loaded (stored internally)
613
621
  if (!modelTriangles) {
614
622
  updateInfo('Model STL must be loaded');
615
623
  return;
616
624
  }
625
+ } else if (mode === 'tracing') {
626
+ // Tracing mode: terrain must be rasterized
627
+ if (!modelRasterData) {
628
+ updateInfo('Model must be rasterized first');
629
+ return;
630
+ }
617
631
  }
618
632
 
619
633
  try {
620
634
  const t0 = performance.now();
621
635
  updateInfo('Generating toolpath...');
622
636
 
623
- // Unified API - works for both modes!
624
- toolpathData = await rasterPath.generateToolpaths({
625
- xStep: xStep,
626
- yStep: yStep,
637
+ // Generate trace paths for tracing mode
638
+ let tracePaths = null;
639
+ if (mode === 'tracing') {
640
+ // Get model bounds from raster data
641
+ const bounds = modelRasterData.bounds;
642
+ const minX = bounds.min.x;
643
+ const maxX = bounds.max.x;
644
+ const minY = bounds.min.y;
645
+ const maxY = bounds.max.y;
646
+
647
+ // Create two cross paths: horizontal through center, vertical through center
648
+ const centerY = (minY + maxY) / 2;
649
+ const centerX = (minX + maxX) / 2;
650
+
651
+ tracePaths = [
652
+ new Float32Array([minX, centerY, maxX, centerY]), // Horizontal line
653
+ new Float32Array([centerX, minY, centerX, maxY]) // Vertical line
654
+ ];
655
+
656
+ debug.log(`Generated trace paths: H(${minX.toFixed(2)}, ${centerY.toFixed(2)}) to (${maxX.toFixed(2)}, ${centerY.toFixed(2)})`);
657
+ debug.log(` V(${centerX.toFixed(2)}, ${minY.toFixed(2)}) to (${centerX.toFixed(2)}, ${maxY.toFixed(2)})`);
658
+ }
659
+
660
+ // Unified API - works for all modes!
661
+ const generateParams = {
627
662
  zFloor: zFloor
628
- });
663
+ };
664
+
665
+ if (mode === 'tracing') {
666
+ generateParams.paths = tracePaths;
667
+ generateParams.step = traceStep;
668
+ } else {
669
+ generateParams.xStep = xStep;
670
+ generateParams.yStep = yStep;
671
+ }
672
+
673
+ toolpathData = await rasterPath.generateToolpaths(generateParams);
629
674
 
630
675
  const t1 = performance.now();
631
676
 
632
677
  if (mode === 'planar') {
633
678
  const numPoints = toolpathData.pathData.length;
634
679
  updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
680
+ } else if (mode === 'tracing') {
681
+ const totalPoints = toolpathData.paths.reduce((sum, path) => sum + path.length / 3, 0);
682
+ debug.log(`[Tracing] Generated ${toolpathData.paths.length} paths with ${totalPoints} total points`);
683
+
684
+ // Log sample Z values from each path
685
+ toolpathData.paths.forEach((path, idx) => {
686
+ const zValues = [];
687
+ for (let i = 2; i < Math.min(path.length, 15); i += 3) {
688
+ zValues.push(path[i].toFixed(2));
689
+ }
690
+ debug.log(`[Tracing] Path ${idx} Z samples:`, zValues.join(', '));
691
+ });
692
+
693
+ updateInfo(`Toolpath generated: ${toolpathData.paths.length} paths, ${totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
635
694
  } else {
636
695
  // debug.log('[Radial] Toolpaths generated:', toolpathData);
637
696
  debug.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
@@ -901,8 +960,8 @@ function displayModelRaster(wrapped) {
901
960
  const positions = [];
902
961
  const colors = [];
903
962
 
904
- if (mode === 'planar') {
905
- // Planar: terrain is dense (Z-only array)
963
+ if (mode === 'planar' || mode === 'tracing') {
964
+ // Planar/Tracing: terrain is dense (Z-only array)
906
965
  const { positions: rasterPos, bounds, gridWidth, gridHeight } = modelRasterData;
907
966
  const stepSize = resolution;
908
967
 
@@ -1057,6 +1116,59 @@ function displayToolpaths(wrapped) {
1057
1116
  return;
1058
1117
  }
1059
1118
 
1119
+ if (mode === 'tracing') {
1120
+ // Tracing toolpaths - array of XYZ paths
1121
+ const { paths } = toolpathData;
1122
+
1123
+ // Calculate total points
1124
+ let totalPoints = 0;
1125
+ for (const path of paths) {
1126
+ totalPoints += path.length / 3;
1127
+ }
1128
+
1129
+ debug.log('[Toolpath Display] Tracing mode:', paths.length, 'paths,', totalPoints, 'total points');
1130
+
1131
+ // Preallocate typed arrays
1132
+ const positions = new Float32Array(totalPoints * 3);
1133
+ const colors = new Float32Array(totalPoints * 3);
1134
+
1135
+ let arrayIdx = 0;
1136
+ for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
1137
+ const path = paths[pathIdx];
1138
+ const numPoints = path.length / 3;
1139
+
1140
+ // Use different colors for each path
1141
+ const color = pathIdx === 0 ? [1, 0.4, 0] : [0, 0.8, 1]; // Orange for horizontal, cyan for vertical
1142
+
1143
+ for (let i = 0; i < numPoints; i++) {
1144
+ positions[arrayIdx] = path[i * 3]; // X
1145
+ positions[arrayIdx + 1] = path[i * 3 + 1]; // Y
1146
+ positions[arrayIdx + 2] = path[i * 3 + 2]; // Z
1147
+
1148
+ colors[arrayIdx] = color[0];
1149
+ colors[arrayIdx + 1] = color[1];
1150
+ colors[arrayIdx + 2] = color[2];
1151
+
1152
+ arrayIdx += 3;
1153
+ }
1154
+ }
1155
+
1156
+ // Create geometry
1157
+ const geometry = new THREE.BufferGeometry();
1158
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1159
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
1160
+
1161
+ const material = new THREE.PointsMaterial({
1162
+ size: resolution * 1.5,
1163
+ vertexColors: true
1164
+ });
1165
+
1166
+ toolpathPoints = new THREE.Points(geometry, material);
1167
+ rotatedGroup.add(toolpathPoints);
1168
+
1169
+ return; // Exit early for tracing mode
1170
+ }
1171
+
1060
1172
  if (mode === 'planar') {
1061
1173
  // Planar toolpaths
1062
1174
  const { pathData, numScanlines, pointsPerLine } = toolpathData;
@@ -1324,15 +1436,32 @@ function updateButtonStates() {
1324
1436
  // ============================================================================
1325
1437
 
1326
1438
  function updateModeUI() {
1327
- // Show/hide wrapped toggle and angle step for radial mode
1439
+ // Show/hide mode-specific controls
1328
1440
  const wrappedContainer = document.getElementById('wrapped-container').classList;
1329
1441
  const angleStepContainer = document.getElementById('angle-step-container').classList;
1442
+ const traceStepContainer = document.getElementById('trace-step-container').classList;
1443
+ const xStepContainer = document.getElementById('x-step-container').classList;
1444
+ const yStepContainer = document.getElementById('y-step-container').classList;
1445
+
1330
1446
  if (mode === 'radial') {
1331
1447
  wrappedContainer.remove('hide');
1332
1448
  angleStepContainer.remove('hide');
1449
+ traceStepContainer.add('hide');
1450
+ xStepContainer.remove('hide');
1451
+ yStepContainer.remove('hide');
1452
+ } else if (mode === 'tracing') {
1453
+ wrappedContainer.add('hide');
1454
+ angleStepContainer.add('hide');
1455
+ traceStepContainer.remove('hide');
1456
+ xStepContainer.add('hide');
1457
+ yStepContainer.add('hide');
1333
1458
  } else {
1459
+ // planar
1334
1460
  wrappedContainer.add('hide');
1335
1461
  angleStepContainer.add('hide');
1462
+ traceStepContainer.add('hide');
1463
+ xStepContainer.remove('hide');
1464
+ yStepContainer.remove('hide');
1336
1465
  }
1337
1466
  }
1338
1467
 
@@ -1452,6 +1581,16 @@ document.addEventListener('DOMContentLoaded', async () => {
1452
1581
  updateButtonStates();
1453
1582
  });
1454
1583
 
1584
+ document.getElementById('trace-step').addEventListener('change', (e) => {
1585
+ traceStep = parseFloat(e.target.value);
1586
+ if (mode === 'tracing') {
1587
+ toolpathData = null; // Need to regenerate toolpath
1588
+ }
1589
+ saveParameters();
1590
+ updateInfo(`Trace Step changed to ${traceStep}mm`);
1591
+ updateButtonStates();
1592
+ });
1593
+
1455
1594
  // Tool size change
1456
1595
  document.getElementById('tool-size').addEventListener('change', async (e) => {
1457
1596
  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,18 @@
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>
91
95
  </div>
92
96
 
93
97
  <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
@@ -221,8 +221,8 @@ export class RasterPath {
221
221
  throw new Error('RasterPath not initialized. Call init() first.');
222
222
  }
223
223
 
224
- if (this.mode === 'planar') {
225
- // Planar: rasterize and return
224
+ if (this.mode === 'planar' || this.mode === 'tracing') {
225
+ // Planar/Tracing: rasterize and return (tracing reuses planar terrain rasterization)
226
226
  const terrainData = await this.#rasterizePlanar({ triangles, zFloor, boundsOverride, isForTool: false, onProgress });
227
227
  this.terrainData = terrainData;
228
228
  return terrainData;
@@ -269,7 +269,7 @@ export class RasterPath {
269
269
  * @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
270
270
  * @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
271
271
  */
272
- async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
272
+ async generateToolpaths({ xStep, yStep, zFloor, onProgress, paths, step }) {
273
273
  if (!this.isInitialized) {
274
274
  throw new Error('RasterPath not initialized. Call init() first.');
275
275
  }
@@ -278,7 +278,7 @@ export class RasterPath {
278
278
  throw new Error('Tool not loaded. Call loadTool() first.');
279
279
  }
280
280
 
281
- debug.log('gen.paths', { xStep, yStep, zFloor });
281
+ debug.log('gen.paths', { xStep, yStep, zFloor, paths: paths?.length, step });
282
282
 
283
283
  if (this.mode === 'planar') {
284
284
  if (!this.terrainData) {
@@ -292,7 +292,7 @@ export class RasterPath {
292
292
  zFloor,
293
293
  onProgress
294
294
  });
295
- } else {
295
+ } else if (this.mode === 'radial') {
296
296
  // Radial mode: use stored triangles
297
297
  if (!this.terrainTriangles) {
298
298
  throw new Error('Terrain not loaded. Call loadTerrain() first.');
@@ -306,14 +306,83 @@ export class RasterPath {
306
306
  zFloor: zFloor ?? this.terrainZFloor,
307
307
  onProgress
308
308
  });
309
+ } else if (this.mode === 'tracing') {
310
+ // Tracing mode: follow input paths
311
+ if (!this.terrainData) {
312
+ throw new Error('Terrain not loaded. Call loadTerrain() first.');
313
+ }
314
+ if (!paths || paths.length === 0) {
315
+ throw new Error('Tracing mode requires paths parameter (array of Float32Array XY coordinates)');
316
+ }
317
+ if (!step || step <= 0) {
318
+ throw new Error('Tracing mode requires step parameter (sampling resolution in world units)');
319
+ }
320
+ return this.#generateToolpathsTracing({
321
+ paths,
322
+ step,
323
+ zFloor,
324
+ onProgress
325
+ });
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Create reusable GPU buffers for tracing mode (optimization for iterative tracing)
331
+ * Call this after loadTerrain() and loadTool() to cache buffers across multiple trace calls
332
+ * @returns {Promise<void>}
333
+ */
334
+ async createTracingBuffers() {
335
+ if (this.mode !== 'tracing') {
336
+ throw new Error('createTracingBuffers() only available in tracing mode');
309
337
  }
338
+ if (!this.terrainData || !this.toolData) {
339
+ throw new Error('Must call loadTerrain() and loadTool() before createTracingBuffers()');
340
+ }
341
+
342
+ return new Promise((resolve, reject) => {
343
+ const handler = () => resolve();
344
+ this.#sendMessage(
345
+ 'create-tracing-buffers',
346
+ {
347
+ terrainPositions: this.terrainData.positions,
348
+ toolPositions: this.toolData.positions
349
+ },
350
+ 'tracing-buffers-created',
351
+ handler
352
+ );
353
+ });
354
+ }
355
+
356
+ /**
357
+ * Destroy reusable tracing buffers
358
+ * @returns {Promise<void>}
359
+ */
360
+ async destroyTracingBuffers() {
361
+ if (this.mode !== 'tracing') {
362
+ return; // No-op for non-tracing modes
363
+ }
364
+
365
+ return new Promise((resolve, reject) => {
366
+ const handler = () => resolve();
367
+ this.#sendMessage(
368
+ 'destroy-tracing-buffers',
369
+ {},
370
+ 'tracing-buffers-destroyed',
371
+ handler
372
+ );
373
+ });
310
374
  }
311
375
 
312
376
  /**
313
377
  * Terminate worker and cleanup resources
314
378
  */
315
- terminate() {
379
+ async terminate() {
316
380
  if (this.worker) {
381
+ // Cleanup tracing buffers if in tracing mode
382
+ if (this.mode === 'tracing') {
383
+ await this.destroyTracingBuffers();
384
+ }
385
+
317
386
  this.worker.terminate();
318
387
  this.worker = null;
319
388
  this.isInitialized = false;
@@ -388,6 +457,46 @@ export class RasterPath {
388
457
  });
389
458
  }
390
459
 
460
+ async #generateToolpathsTracing({ paths, step, zFloor, onProgress }) {
461
+ return new Promise((resolve, reject) => {
462
+ // Set up progress handler if callback provided
463
+ if (onProgress) {
464
+ const progressHandler = (data) => {
465
+ onProgress(data.percent, { current: data.current, total: data.total, pathIndex: data.pathIndex });
466
+ };
467
+ this.messageHandlers.set('tracing-progress', progressHandler);
468
+ }
469
+
470
+ const handler = (data) => {
471
+ // Clean up progress handler
472
+ if (onProgress) {
473
+ this.messageHandlers.delete('tracing-progress');
474
+ }
475
+ resolve(data);
476
+ };
477
+
478
+ this.#sendMessage(
479
+ 'tracing-generate-toolpaths',
480
+ {
481
+ paths,
482
+ terrainPositions: this.terrainData.positions,
483
+ terrainData: {
484
+ width: this.terrainData.gridWidth,
485
+ height: this.terrainData.gridHeight,
486
+ bounds: this.terrainData.bounds
487
+ },
488
+ toolPositions: this.toolData.positions,
489
+ step,
490
+ gridStep: this.resolution,
491
+ terrainBounds: this.terrainData.bounds,
492
+ zFloor: zFloor ?? 0
493
+ },
494
+ 'tracing-toolpaths-complete',
495
+ handler
496
+ );
497
+ });
498
+ }
499
+
391
500
  async #generateToolpathsRadial({ triangles, bounds, toolData, xStep, yStep, zFloor, onProgress }) {
392
501
  const maxRadius = this.#calculateMaxRadius(triangles);
393
502