@gridspace/raster-path 1.0.2 → 1.0.4

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/src/web/app.js CHANGED
@@ -12,12 +12,16 @@ let zFloor = -100;
12
12
  let xStep = 5;
13
13
  let yStep = 5;
14
14
  let angleStep = 1.0; // degrees
15
+ let toolSize = 2.5; // mm - target tool diameter
15
16
 
16
- let modelSTL = null; // ArrayBuffer
17
+ let modelSTL = null; // ArrayBuffer (current, possibly rotated)
18
+ let modelOriginalSTL = null; // ArrayBuffer (original, for reset)
17
19
  let toolSTL = null; // ArrayBuffer
20
+ let toolOriginalSTL = null; // ArrayBuffer (original, for reset/scaling)
18
21
 
19
22
  let modelTriangles = null; // Float32Array
20
23
  let toolTriangles = null; // Float32Array
24
+ let toolOriginalTriangles = null; // Float32Array (original, unscaled)
21
25
 
22
26
  let modelRasterData = null;
23
27
  let toolRasterData = null;
@@ -26,6 +30,9 @@ let toolpathData = null;
26
30
  let modelMaxZ = 0; // Track max Z for tool offset
27
31
  let rasterPath = null; // RasterPath instance
28
32
 
33
+ // Model rotation state (accumulated 90-degree rotations)
34
+ let modelRotation = { x: 0, y: 0, z: 0 }; // In 90-degree increments
35
+
29
36
  // Three.js objects
30
37
  let scene, camera, renderer, controls;
31
38
  let rotatedGroup = null; // Group for 90-degree rotation
@@ -35,6 +42,14 @@ let modelRasterPoints = null;
35
42
  let toolRasterPoints = null;
36
43
  let toolpathPoints = null;
37
44
 
45
+ const log_pre = '[App]';
46
+ const debug = {
47
+ error: function() { console.error(log_pre, ...arguments) },
48
+ warn: function() { console.warn(log_pre, ...arguments) },
49
+ log: function() { console.log(log_pre, ...arguments) },
50
+ ok: function() { console.log(log_pre, '✅', ...arguments) },
51
+ };
52
+
38
53
  // ============================================================================
39
54
  // Parameter Persistence
40
55
  // ============================================================================
@@ -46,6 +61,8 @@ function saveParameters() {
46
61
  localStorage.setItem('raster-xStep', xStep);
47
62
  localStorage.setItem('raster-yStep', yStep);
48
63
  localStorage.setItem('raster-angleStep', angleStep);
64
+ localStorage.setItem('raster-toolSize', toolSize);
65
+ console.log(`[App] Saved tool size: ${toolSize}mm`);
49
66
 
50
67
  // Save view checkboxes
51
68
  const showWrappedCheckbox = document.getElementById('show-wrapped');
@@ -110,6 +127,16 @@ function loadParameters() {
110
127
  document.getElementById('angle-step').value = angleStep;
111
128
  }
112
129
 
130
+ const savedToolSize = localStorage.getItem('raster-toolSize');
131
+ if (savedToolSize !== null) {
132
+ toolSize = parseFloat(savedToolSize);
133
+ // Format to match dropdown option values (e.g., "3.0" not "3")
134
+ document.getElementById('tool-size').value = toolSize.toFixed(1);
135
+ console.log(`[App] Restored tool size: ${toolSize}mm`);
136
+ } else {
137
+ console.log(`[App] No saved tool size, using default: ${toolSize}mm`);
138
+ }
139
+
113
140
  // Restore view checkboxes
114
141
  const savedShowWrapped = localStorage.getItem('raster-showWrapped');
115
142
  if (savedShowWrapped !== null) {
@@ -187,6 +214,37 @@ function calculateTriangleBounds(triangles) {
187
214
  return bounds;
188
215
  }
189
216
 
217
+ // Calculate current tool diameter from XY dimensions
218
+ function calculateToolDiameter(triangles) {
219
+ if (!triangles || triangles.length === 0) return 0;
220
+
221
+ const bounds = calculateTriangleBounds(triangles);
222
+ const xSize = bounds.max.x - bounds.min.x;
223
+ const ySize = bounds.max.y - bounds.min.y;
224
+
225
+ // Return the maximum of X and Y dimensions as the diameter
226
+ return Math.max(xSize, ySize);
227
+ }
228
+
229
+ // Scale tool triangles to target diameter
230
+ function scaleToolTriangles(triangles, targetDiameter) {
231
+ if (!triangles || triangles.length === 0) return triangles;
232
+
233
+ const currentDiameter = calculateToolDiameter(triangles);
234
+ if (currentDiameter === 0) return triangles;
235
+
236
+ const scale = targetDiameter / currentDiameter;
237
+ const scaled = new Float32Array(triangles.length);
238
+
239
+ for (let i = 0; i < triangles.length; i += 3) {
240
+ scaled[i] = triangles[i] * scale; // X
241
+ scaled[i + 1] = triangles[i + 1] * scale; // Y
242
+ scaled[i + 2] = triangles[i + 2] * scale; // Z
243
+ }
244
+
245
+ return scaled;
246
+ }
247
+
190
248
  function parseSTL(arrayBuffer) {
191
249
  const view = new DataView(arrayBuffer);
192
250
 
@@ -246,6 +304,173 @@ function parseASCIISTL(arrayBuffer) {
246
304
  return new Float32Array(triangles);
247
305
  }
248
306
 
307
+ // ============================================================================
308
+ // STL Creation from Triangles
309
+ // ============================================================================
310
+
311
+ function createSTLFromTriangles(triangles) {
312
+ // Create binary STL from triangle array
313
+ const numTriangles = triangles.length / 9;
314
+ const bufferSize = 80 + 4 + numTriangles * 50; // header + count + triangles
315
+ const buffer = new ArrayBuffer(bufferSize);
316
+ const view = new DataView(buffer);
317
+
318
+ // Write header (80 bytes, can be anything)
319
+ const headerText = 'Binary STL - rotated model';
320
+ for (let i = 0; i < Math.min(headerText.length, 80); i++) {
321
+ view.setUint8(i, headerText.charCodeAt(i));
322
+ }
323
+
324
+ // Write triangle count
325
+ view.setUint32(80, numTriangles, true);
326
+
327
+ // Write triangles
328
+ let offset = 84;
329
+ for (let i = 0; i < triangles.length; i += 9) {
330
+ // Calculate normal (simplified - not computing actual normal)
331
+ view.setFloat32(offset, 0, true); offset += 4; // nx
332
+ view.setFloat32(offset, 0, true); offset += 4; // ny
333
+ view.setFloat32(offset, 1, true); offset += 4; // nz
334
+
335
+ // Write vertices
336
+ for (let j = 0; j < 9; j++) {
337
+ view.setFloat32(offset, triangles[i + j], true);
338
+ offset += 4;
339
+ }
340
+
341
+ // Attribute byte count
342
+ view.setUint16(offset, 0, true);
343
+ offset += 2;
344
+ }
345
+
346
+ return buffer;
347
+ }
348
+
349
+ // ============================================================================
350
+ // Model Rotation
351
+ // ============================================================================
352
+
353
+ function rotateTriangles90(triangles, axis, direction) {
354
+ // direction: 1 for +90°, -1 for -90°
355
+ // Rotates triangle vertices around the specified axis
356
+ const result = new Float32Array(triangles.length);
357
+ const angle = direction * Math.PI / 2; // 90 degrees in radians
358
+ const cos = Math.cos(angle);
359
+ const sin = Math.sin(angle);
360
+
361
+ for (let i = 0; i < triangles.length; i += 3) {
362
+ const x = triangles[i];
363
+ const y = triangles[i + 1];
364
+ const z = triangles[i + 2];
365
+
366
+ if (axis === 'x') {
367
+ // Rotate around X-axis: y' = y*cos - z*sin, z' = y*sin + z*cos
368
+ result[i] = x;
369
+ result[i + 1] = y * cos - z * sin;
370
+ result[i + 2] = y * sin + z * cos;
371
+ } else if (axis === 'y') {
372
+ // Rotate around Y-axis: x' = x*cos + z*sin, z' = -x*sin + z*cos
373
+ result[i] = x * cos + z * sin;
374
+ result[i + 1] = y;
375
+ result[i + 2] = -x * sin + z * cos;
376
+ } else if (axis === 'z') {
377
+ // Rotate around Z-axis: x' = x*cos - y*sin, y' = x*sin + y*cos
378
+ result[i] = x * cos - y * sin;
379
+ result[i + 1] = x * sin + y * cos;
380
+ result[i + 2] = z;
381
+ }
382
+ }
383
+
384
+ return result;
385
+ }
386
+
387
+ function applyModelRotation(axis, direction) {
388
+ if (!modelMesh) {
389
+ updateInfo('No model loaded');
390
+ return;
391
+ }
392
+
393
+ // Update rotation state
394
+ modelRotation[axis] = (modelRotation[axis] + direction) % 4;
395
+ if (modelRotation[axis] < 0) modelRotation[axis] += 4;
396
+
397
+ // Rotate the mesh using Three.js
398
+ const angle = direction * Math.PI / 2;
399
+ if (axis === 'x') {
400
+ modelMesh.rotateX(-angle);
401
+ } else if (axis === 'y') {
402
+ modelMesh.rotateY(angle);
403
+ } else if (axis === 'z') {
404
+ modelMesh.rotateZ(-angle);
405
+ }
406
+
407
+ let { geometry } = modelMesh;
408
+
409
+ modelMesh.updateMatrix();
410
+ geometry.applyMatrix4(modelMesh.matrix);
411
+ modelMesh.position.set(0, 0, 0);
412
+ modelMesh.rotation.set(0, 0, 0);
413
+ modelMesh.scale.set(1, 1, 1);
414
+ modelMesh.updateMatrixWorld(true);
415
+ geometry.attributes.position.needsUpdate = true;
416
+ geometry.computeVertexNormals();
417
+ geometry.computeBoundingBox();
418
+ geometry.computeBoundingSphere();
419
+
420
+ // Clear raster and toolpath data (needs recomputation)
421
+ modelRasterData = null;
422
+ toolpathData = null;
423
+
424
+ // Update cached STL with rotated triangles
425
+ modelSTL = createSTLFromTriangles(modelTriangles);
426
+ cacheSTL('model-stl', modelSTL, document.getElementById('model-status').textContent || 'model.stl')
427
+ .catch(err => debug.warn('Failed to cache rotated model:', err));
428
+
429
+ updateButtonStates();
430
+ updateInfo(`Model rotated ${direction > 0 ? '+' : ''}${direction * 90}° around ${axis.toUpperCase()}`);
431
+ }
432
+
433
+ function resetModelRotation() {
434
+ if (!modelMesh || !modelOriginalSTL) {
435
+ updateInfo('No model loaded');
436
+ return;
437
+ }
438
+
439
+ // Re-parse from ORIGINAL STL (not current/rotated)
440
+ modelTriangles = parseSTL(modelOriginalSTL);
441
+ modelSTL = modelOriginalSTL; // Reset current to original
442
+ modelRotation = { x: 0, y: 0, z: 0 };
443
+
444
+ // Update the existing mesh geometry
445
+ const positionAttr = modelMesh.geometry.attributes.position;
446
+ for (let i = 0; i < positionAttr.count; i++) {
447
+ positionAttr.setXYZ(i,
448
+ modelTriangles[i * 3],
449
+ modelTriangles[i * 3 + 1],
450
+ modelTriangles[i * 3 + 2]
451
+ );
452
+ }
453
+ positionAttr.needsUpdate = true;
454
+ modelMesh.geometry.computeVertexNormals();
455
+ modelMesh.geometry.computeBoundingBox();
456
+
457
+ // Reset mesh rotation
458
+ modelMesh.rotation.set(0, 0, 0);
459
+ modelMesh.updateMatrix();
460
+ modelMesh.updateMatrixWorld(true);
461
+
462
+ // Clear raster and toolpath data
463
+ modelRasterData = null;
464
+ toolpathData = null;
465
+
466
+ // Update cache with original STL
467
+ cacheSTL('model-stl', modelSTL, document.getElementById('model-status').textContent || 'model.stl')
468
+ .catch(err => debug.warn('Failed to cache reset model:', err));
469
+
470
+ updateButtonStates();
471
+ updateInfo('Model rotation reset');
472
+ }
473
+
249
474
  // ============================================================================
250
475
  // File Loading
251
476
  // ============================================================================
@@ -267,6 +492,10 @@ async function loadSTLFile(isModel) {
267
492
  const cacheKey = isModel ? 'model-stl' : 'tool-stl';
268
493
  await cacheSTL(cacheKey, arrayBuffer, file.name);
269
494
 
495
+ modelRasterData = undefined;
496
+ toolpathData = undefined;
497
+ updateVisualization();
498
+
270
499
  updateInfo(`Loaded ${file.name}: ${(triangles.length / 9).toLocaleString()} triangles`);
271
500
  resolve({ arrayBuffer, triangles, name: file.name });
272
501
  };
@@ -288,8 +517,8 @@ async function initRasterPath() {
288
517
  mode: mode,
289
518
  resolution: resolution,
290
519
  rotationStep: mode === 'radial' ? angleStep : undefined,
291
- // trianglesPerTile: 15000,
292
- debug: true // Enable debug logging
520
+ batchDivisor: 5,
521
+ debug: true
293
522
  });
294
523
 
295
524
  await rasterPath.init();
@@ -308,7 +537,6 @@ async function rasterizeAll() {
308
537
 
309
538
  // Load tool (works for both modes)
310
539
  if (toolTriangles) {
311
- updateInfo('Loading tool...');
312
540
  const t0 = performance.now();
313
541
  toolRasterData = await rasterPath.loadTool({
314
542
  triangles: toolTriangles
@@ -338,14 +566,13 @@ async function rasterizeAll() {
338
566
 
339
567
  // Load terrain (stores triangles for later, doesn't rasterize yet)
340
568
  if (modelTriangles) {
341
- updateInfo('Loading terrain...');
342
569
  const t0 = performance.now();
343
570
  await rasterPath.loadTerrain({
344
571
  triangles: modelTriangles,
345
572
  zFloor: zFloor
346
573
  });
347
574
  const t1 = performance.now();
348
- updateInfo(`Terrain loaded in ${(t1 - t0).toFixed(0)}ms (will rasterize during toolpath generation)`);
575
+ updateInfo(`Terrain loaded in ${(t1 - t0).toFixed(0)}ms (rasterized in toolpath generation)`);
349
576
  } else {
350
577
  updateInfo('Tool loaded. Load model and click "Generate Toolpath" to continue.');
351
578
  }
@@ -364,7 +591,7 @@ async function rasterizeAll() {
364
591
  updateButtonStates();
365
592
 
366
593
  } catch (error) {
367
- console.error('Rasterization error:', error);
594
+ debug.error('Rasterization error:', error);
368
595
  updateInfo(`Error: ${error.message}`);
369
596
  }
370
597
  }
@@ -406,19 +633,19 @@ async function generateToolpath() {
406
633
  const numPoints = toolpathData.pathData.length;
407
634
  updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
408
635
  } else {
409
- console.log('Radial toolpaths generated:', toolpathData);
410
- console.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
636
+ // debug.log('[Radial] Toolpaths generated:', toolpathData);
637
+ debug.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
411
638
  updateInfo(`Toolpath generated: ${toolpathData.numStrips} strips, ${toolpathData.totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
412
639
 
413
640
  // Store terrain strips for visualization
414
641
  if (toolpathData.terrainStrips) {
415
642
  modelRasterData = toolpathData.terrainStrips;
416
- console.log('[Radial] Terrain strips available:', modelRasterData.length, 'strips');
643
+ debug.log('[Radial] Terrain strips available:', modelRasterData.length, 'strips');
417
644
 
418
645
  // DEBUG: Check X range in strips
419
646
  if (modelRasterData.length > 0) {
420
647
  const strip0 = modelRasterData[0];
421
- console.log('[Radial] Strip 0 structure:', {
648
+ debug.log('[Radial] Strip 0 structure:', {
422
649
  angle: strip0.angle,
423
650
  pointCount: strip0.pointCount,
424
651
  positionsLength: strip0.positions?.length,
@@ -435,7 +662,7 @@ async function generateToolpath() {
435
662
  minX = Math.min(minX, x);
436
663
  maxX = Math.max(maxX, x);
437
664
  }
438
- console.log('[Radial] Strip 0 actual X range in positions:', minX.toFixed(2), 'to', maxX.toFixed(2));
665
+ debug.log('[Radial] Strip 0 actual X range in positions:', minX.toFixed(2), 'to', maxX.toFixed(2));
439
666
  }
440
667
  }
441
668
  } else if (toolpathData.strips && toolpathData.strips[0]?.terrainBounds) {
@@ -444,7 +671,7 @@ async function generateToolpath() {
444
671
  angle: strip.angle,
445
672
  bounds: strip.terrainBounds
446
673
  }));
447
- console.log('[Radial] Batched mode: created synthetic terrain strips from bounds:', modelRasterData.length, 'strips');
674
+ debug.log('[Radial] Batched mode: created synthetic terrain strips from bounds:', modelRasterData.length, 'strips');
448
675
  }
449
676
  // This is fine - we only need toolpathData for visualization
450
677
  }
@@ -455,7 +682,7 @@ async function generateToolpath() {
455
682
  updateVisualization();
456
683
 
457
684
  } catch (error) {
458
- console.error('Toolpath generation error:', error);
685
+ debug.error('Toolpath generation error:', error);
459
686
  updateInfo(`Error: ${error.message}`);
460
687
  }
461
688
  }
@@ -610,6 +837,14 @@ function updateVisualization() {
610
837
  function displayModelMesh() {
611
838
  if (!modelTriangles) return;
612
839
 
840
+ // Remove old mesh if it exists
841
+ if (modelMesh) {
842
+ rotatedGroup.remove(modelMesh);
843
+ modelMesh.geometry.dispose();
844
+ modelMesh.material.dispose();
845
+ modelMesh = null;
846
+ }
847
+
613
848
  const geometry = new THREE.BufferGeometry();
614
849
  geometry.setAttribute('position', new THREE.BufferAttribute(modelTriangles, 3));
615
850
  geometry.computeVertexNormals();
@@ -635,6 +870,14 @@ function displayModelMesh() {
635
870
  function displayToolMesh() {
636
871
  if (!toolTriangles) return;
637
872
 
873
+ // Remove old mesh if it exists
874
+ if (toolMesh) {
875
+ rotatedGroup.remove(toolMesh);
876
+ toolMesh.geometry.dispose();
877
+ toolMesh.material.dispose();
878
+ toolMesh = null;
879
+ }
880
+
638
881
  const geometry = new THREE.BufferGeometry();
639
882
  geometry.setAttribute('position', new THREE.BufferAttribute(toolTriangles, 3));
640
883
  geometry.computeVertexNormals();
@@ -682,13 +925,13 @@ function displayModelRaster(wrapped) {
682
925
  // Radial: modelRasterData is an array of strips
683
926
  // Each strip has: { angle, positions (sparse XYZ), gridWidth, gridHeight, bounds }
684
927
  if (!Array.isArray(modelRasterData)) {
685
- console.error('[Display] modelRasterData is not an array of strips');
928
+ debug.error('[Display] modelRasterData is not an array of strips');
686
929
  return;
687
930
  }
688
931
 
689
932
  if (wrapped) {
690
933
  // Wrap each strip around X-axis at its angle
691
- console.log('[Display] Wrapping', modelRasterData.length, 'strips');
934
+ debug.log('[Display] Wrapping', modelRasterData.length, 'strips');
692
935
 
693
936
  // Track overall X range for debug
694
937
  let overallMinX = Infinity, overallMaxX = -Infinity;
@@ -722,14 +965,14 @@ function displayModelRaster(wrapped) {
722
965
  }
723
966
  }
724
967
 
725
- console.log('[Display] Rendered', totalPointsRendered, 'points from', modelRasterData.length, 'strips');
726
- console.log('[Display] X range: [' + overallMinX.toFixed(2) + ', ' + overallMaxX.toFixed(2) + ']');
968
+ debug.log('[Display] Rendered', totalPointsRendered, 'points from', modelRasterData.length, 'strips');
969
+ debug.log('[Display] X range: [' + overallMinX.toFixed(2) + ', ' + overallMaxX.toFixed(2) + ']');
727
970
 
728
971
  // DEBUG: Check angle coverage
729
972
  const angles = modelRasterData.map(s => s.angle).sort((a, b) => a - b);
730
- console.log('[Display] Angle range: [' + angles[0].toFixed(1) + '°, ' + angles[angles.length-1].toFixed(1) + '°]');
731
- console.log('[Display] First 5 angles:', angles.slice(0, 5).map(a => a.toFixed(1) + '°').join(', '));
732
- console.log('[Display] Last 5 angles:', angles.slice(-5).map(a => a.toFixed(1) + '°').join(', '));
973
+ debug.log('[Display] Angle range: [' + angles[0].toFixed(1) + '°, ' + angles[angles.length-1].toFixed(1) + '°]');
974
+ debug.log('[Display] First 5 angles:', angles.slice(0, 5).map(a => a.toFixed(1) + '°').join(', '));
975
+ debug.log('[Display] Last 5 angles:', angles.slice(-5).map(a => a.toFixed(1) + '°').join(', '));
733
976
  } else {
734
977
  // Show unwrapped (planar) - lay out strips side by side
735
978
  for (let stripIdx = 0; stripIdx < modelRasterData.length; stripIdx++) {
@@ -810,7 +1053,9 @@ function displayToolRaster() {
810
1053
  }
811
1054
 
812
1055
  function displayToolpaths(wrapped) {
813
- if (!toolpathData) return;
1056
+ if (!toolpathData) {
1057
+ return;
1058
+ }
814
1059
 
815
1060
  if (mode === 'planar') {
816
1061
  // Planar toolpaths
@@ -823,9 +1068,9 @@ function displayToolpaths(wrapped) {
823
1068
 
824
1069
  // Check if we need to downsample
825
1070
  if (totalPoints > MAX_DISPLAY_POINTS) {
826
- console.warn(`[Toolpath Display] Toolpath too large for visualization: ${(totalPoints/1e6).toFixed(1)}M points`);
827
- console.warn(`[Toolpath Display] Skipping display (max: ${(MAX_DISPLAY_POINTS/1e6).toFixed(1)}M points)`);
828
- console.warn(`[Toolpath Display] Toolpath was generated successfully - only visualization is skipped`);
1071
+ debug.warn(`[Toolpath Display] Toolpath too large for visualization: ${(totalPoints/1e6).toFixed(1)}M points`);
1072
+ debug.warn(`[Toolpath Display] Skipping display (max: ${(MAX_DISPLAY_POINTS/1e6).toFixed(1)}M points)`);
1073
+ debug.warn(`[Toolpath Display] Toolpath was generated successfully - only visualization is skipped`);
829
1074
  return;
830
1075
  }
831
1076
 
@@ -883,27 +1128,27 @@ function displayToolpaths(wrapped) {
883
1128
 
884
1129
  const MAX_DISPLAY_POINTS = 10000000; // 10M points max for display
885
1130
 
886
- console.log('[Toolpath Display] Radial V2 mode:', strips.length, 'strips,', totalPoints, 'total points');
1131
+ debug.log('[Toolpath Display] Radial V2 mode:', strips.length, 'strips,', totalPoints, 'total points');
887
1132
 
888
1133
  // Check if we need to skip visualization
889
1134
  if (totalPoints > MAX_DISPLAY_POINTS) {
890
- console.warn(`[Toolpath Display] Toolpath too large for visualization: ${(totalPoints/1e6).toFixed(1)}M points`);
891
- console.warn(`[Toolpath Display] Skipping display (max: ${(MAX_DISPLAY_POINTS/1e6).toFixed(1)}M points)`);
892
- console.warn(`[Toolpath Display] Toolpath was generated successfully - only visualization is skipped`);
1135
+ debug.warn(`[Toolpath Display] Toolpath too large for visualization: ${(totalPoints/1e6).toFixed(1)}M points`);
1136
+ debug.warn(`[Toolpath Display] Skipping display (max: ${(MAX_DISPLAY_POINTS/1e6).toFixed(1)}M points)`);
1137
+ debug.warn(`[Toolpath Display] Toolpath was generated successfully - only visualization is skipped`);
893
1138
  return;
894
1139
  }
895
1140
 
896
1141
  // DEBUG: Check angle distribution AND data
897
1142
  if (strips.length > 0) {
898
1143
  const angleChecks = [0, 180, 359, 360, 361, 540, 719].filter(i => i < strips.length);
899
- console.log('[Toolpath Display] Angle check at indices:', angleChecks.map(i => `${i}=${strips[i].angle.toFixed(1)}°`).join(', '));
1144
+ debug.log('[Toolpath Display] Angle check at indices:', angleChecks.map(i => `${i}=${strips[i].angle.toFixed(1)}°`).join(', '));
900
1145
  // Check if pathData is actually different between strips
901
1146
  if (strips.length > 360) {
902
1147
  const samples0 = strips[0].pathData.slice(0, 5).map(v => v.toFixed(3)).join(',');
903
1148
  const samples360 = strips[360].pathData.slice(0, 5).map(v => v.toFixed(3)).join(',');
904
- console.log('[Toolpath Display] Data check: strip 0 first 5 values:', samples0);
905
- console.log('[Toolpath Display] Data check: strip 360 first 5 values:', samples360);
906
- console.log('[Toolpath Display] Data is', samples0 === samples360 ? 'SAME (BUG!)' : 'DIFFERENT (OK)');
1149
+ debug.log('[Toolpath Display] Data check: strip 0 first 5 values:', samples0);
1150
+ debug.log('[Toolpath Display] Data check: strip 360 first 5 values:', samples360);
1151
+ debug.log('[Toolpath Display] Data is', samples0 === samples360 ? 'SAME (BUG!)' : 'DIFFERENT (OK)');
907
1152
  }
908
1153
  }
909
1154
 
@@ -919,7 +1164,7 @@ function displayToolpaths(wrapped) {
919
1164
  maxVal = Math.max(maxVal, firstStrip.pathData[i]);
920
1165
  }
921
1166
 
922
- console.log('[Toolpath Display] First strip sample:', {
1167
+ debug.log('[Toolpath Display] First strip sample:', {
923
1168
  angle: firstStrip.angle,
924
1169
  numScanlines: firstStrip.numScanlines,
925
1170
  pointsPerLine: firstStrip.pointsPerLine,
@@ -1051,7 +1296,7 @@ function displayToolpaths(wrapped) {
1051
1296
  // ============================================================================
1052
1297
 
1053
1298
  function updateInfo(text) {
1054
- console.log(text);
1299
+ debug.log(text);
1055
1300
  document.getElementById('info').textContent = text;
1056
1301
  }
1057
1302
 
@@ -1104,6 +1349,7 @@ document.addEventListener('DOMContentLoaded', async () => {
1104
1349
  // Handle both old format (raw ArrayBuffer) and new format (object with arrayBuffer and name)
1105
1350
  const isOldFormat = cachedModel instanceof ArrayBuffer;
1106
1351
  modelSTL = isOldFormat ? cachedModel : cachedModel.arrayBuffer;
1352
+ modelOriginalSTL = modelSTL; // Cache might have rotated model, but treat as original for now
1107
1353
  modelTriangles = parseSTL(modelSTL);
1108
1354
  document.getElementById('model-status').textContent = isOldFormat ? 'Cached model' : (cachedModel.name || 'Cached model');
1109
1355
  displayModelMesh();
@@ -1113,9 +1359,19 @@ document.addEventListener('DOMContentLoaded', async () => {
1113
1359
  if (cachedTool) {
1114
1360
  // Handle both old format (raw ArrayBuffer) and new format (object with arrayBuffer and name)
1115
1361
  const isOldFormat = cachedTool instanceof ArrayBuffer;
1116
- toolSTL = isOldFormat ? cachedTool : cachedTool.arrayBuffer;
1117
- toolTriangles = parseSTL(toolSTL);
1362
+ toolOriginalSTL = isOldFormat ? cachedTool : cachedTool.arrayBuffer;
1363
+ toolOriginalTriangles = parseSTL(toolOriginalSTL);
1364
+
1365
+ // Scale tool to target size
1366
+ const originalDiameter = calculateToolDiameter(toolOriginalTriangles);
1367
+ toolTriangles = scaleToolTriangles(toolOriginalTriangles, toolSize);
1368
+ toolSTL = createSTLFromTriangles(toolTriangles);
1369
+
1370
+ // Update status
1118
1371
  document.getElementById('tool-status').textContent = isOldFormat ? 'Cached tool' : (cachedTool.name || 'Cached tool');
1372
+ document.getElementById('tool-size-status').textContent =
1373
+ `Original: ${originalDiameter.toFixed(2)}mm → Scaled: ${toolSize}mm`;
1374
+
1119
1375
  displayToolMesh();
1120
1376
  }
1121
1377
 
@@ -1196,12 +1452,71 @@ document.addEventListener('DOMContentLoaded', async () => {
1196
1452
  updateButtonStates();
1197
1453
  });
1198
1454
 
1455
+ // Tool size change
1456
+ document.getElementById('tool-size').addEventListener('change', async (e) => {
1457
+ toolSize = parseFloat(e.target.value);
1458
+
1459
+ // If tool is loaded, rescale it
1460
+ if (toolOriginalTriangles) {
1461
+ const originalDiameter = calculateToolDiameter(toolOriginalTriangles);
1462
+ toolTriangles = scaleToolTriangles(toolOriginalTriangles, toolSize);
1463
+ toolSTL = createSTLFromTriangles(toolTriangles);
1464
+
1465
+ // Update status
1466
+ document.getElementById('tool-size-status').textContent =
1467
+ `Original: ${originalDiameter.toFixed(2)}mm → Scaled: ${toolSize}mm`;
1468
+
1469
+ // Check if tool was already loaded (before clearing)
1470
+ const wasToolLoaded = toolRasterData !== null;
1471
+
1472
+ // Clear raster data (tool size changed)
1473
+ toolRasterData = null;
1474
+ toolpathData = null;
1475
+
1476
+ displayToolMesh();
1477
+
1478
+ // If rasterPath exists and tool was already loaded, reload it with new size
1479
+ if (rasterPath && wasToolLoaded) {
1480
+ updateInfo(`Reloading tool at ${toolSize}mm...`);
1481
+ try {
1482
+ const t0 = performance.now();
1483
+ toolRasterData = await rasterPath.loadTool({
1484
+ triangles: toolTriangles
1485
+ });
1486
+ const t1 = performance.now();
1487
+ updateInfo(`Tool reloaded at ${toolSize}mm in ${(t1 - t0).toFixed(0)}ms`);
1488
+ } catch (error) {
1489
+ updateInfo(`Error reloading tool: ${error.message}`);
1490
+ }
1491
+ } else {
1492
+ updateInfo(`Tool size changed to ${toolSize}mm - click Rasterize to apply`);
1493
+ }
1494
+
1495
+ updateButtonStates();
1496
+ }
1497
+
1498
+ saveParameters();
1499
+ });
1500
+
1501
+ // Model rotation buttons
1502
+ document.querySelectorAll('.rotate-btn').forEach(btn => {
1503
+ btn.addEventListener('click', () => {
1504
+ const axis = btn.dataset.axis;
1505
+ const direction = parseInt(btn.dataset.dir);
1506
+ applyModelRotation(axis, direction);
1507
+ });
1508
+ });
1509
+
1510
+ document.getElementById('reset-rotation').addEventListener('click', resetModelRotation);
1511
+
1199
1512
  // Load Model button
1200
1513
  document.getElementById('load-model').addEventListener('click', async () => {
1201
1514
  const result = await loadSTLFile(true);
1202
1515
  if (result) {
1203
1516
  modelSTL = result.arrayBuffer;
1517
+ modelOriginalSTL = result.arrayBuffer; // Save original for reset
1204
1518
  modelTriangles = result.triangles;
1519
+ modelRotation = { x: 0, y: 0, z: 0 }; // Reset rotation on new model
1205
1520
  document.getElementById('model-status').textContent = result.name;
1206
1521
 
1207
1522
  // Clear raster data
@@ -1217,9 +1532,18 @@ document.addEventListener('DOMContentLoaded', async () => {
1217
1532
  document.getElementById('load-tool').addEventListener('click', async () => {
1218
1533
  const result = await loadSTLFile(false);
1219
1534
  if (result) {
1220
- toolSTL = result.arrayBuffer;
1221
- toolTriangles = result.triangles;
1535
+ toolOriginalSTL = result.arrayBuffer;
1536
+ toolOriginalTriangles = result.triangles;
1537
+
1538
+ // Calculate original diameter and scale to target size
1539
+ const originalDiameter = calculateToolDiameter(toolOriginalTriangles);
1540
+ toolTriangles = scaleToolTriangles(toolOriginalTriangles, toolSize);
1541
+ toolSTL = createSTLFromTriangles(toolTriangles);
1542
+
1543
+ // Update status with size info
1222
1544
  document.getElementById('tool-status').textContent = result.name;
1545
+ document.getElementById('tool-size-status').textContent =
1546
+ `Original: ${originalDiameter.toFixed(2)}mm → Scaled: ${toolSize}mm`;
1223
1547
 
1224
1548
  // Clear raster data
1225
1549
  toolRasterData = null;