@gridspace/raster-path 1.0.2

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 ADDED
@@ -0,0 +1,1254 @@
1
+ import * as THREE from 'three';
2
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
+ import { RasterPath } from './raster-path.js';
4
+
5
+ // ============================================================================
6
+ // State
7
+ // ============================================================================
8
+
9
+ let mode = 'planar';
10
+ let resolution = 0.1;
11
+ let zFloor = -100;
12
+ let xStep = 5;
13
+ let yStep = 5;
14
+ let angleStep = 1.0; // degrees
15
+
16
+ let modelSTL = null; // ArrayBuffer
17
+ let toolSTL = null; // ArrayBuffer
18
+
19
+ let modelTriangles = null; // Float32Array
20
+ let toolTriangles = null; // Float32Array
21
+
22
+ let modelRasterData = null;
23
+ let toolRasterData = null;
24
+ let toolpathData = null;
25
+
26
+ let modelMaxZ = 0; // Track max Z for tool offset
27
+ let rasterPath = null; // RasterPath instance
28
+
29
+ // Three.js objects
30
+ let scene, camera, renderer, controls;
31
+ let rotatedGroup = null; // Group for 90-degree rotation
32
+ let modelMesh = null;
33
+ let toolMesh = null;
34
+ let modelRasterPoints = null;
35
+ let toolRasterPoints = null;
36
+ let toolpathPoints = null;
37
+
38
+ // ============================================================================
39
+ // Parameter Persistence
40
+ // ============================================================================
41
+
42
+ function saveParameters() {
43
+ localStorage.setItem('raster-mode', mode);
44
+ localStorage.setItem('raster-resolution', resolution);
45
+ localStorage.setItem('raster-zFloor', zFloor);
46
+ localStorage.setItem('raster-xStep', xStep);
47
+ localStorage.setItem('raster-yStep', yStep);
48
+ localStorage.setItem('raster-angleStep', angleStep);
49
+
50
+ // Save view checkboxes
51
+ const showWrappedCheckbox = document.getElementById('show-wrapped');
52
+ if (showWrappedCheckbox) {
53
+ localStorage.setItem('raster-showWrapped', showWrappedCheckbox.checked);
54
+ }
55
+ }
56
+
57
+ function loadParameters() {
58
+ const savedMode = localStorage.getItem('raster-mode');
59
+ if (savedMode !== null) {
60
+ mode = savedMode;
61
+ const modeRadio = document.querySelector(`input[name="mode"][value="${mode}"]`);
62
+ if (modeRadio) {
63
+ modeRadio.checked = true;
64
+ // Trigger mode change to update UI visibility
65
+ updateModeUI();
66
+ }
67
+ }
68
+
69
+ const savedResolution = localStorage.getItem('raster-resolution');
70
+ if (savedResolution !== null) {
71
+ resolution = parseFloat(savedResolution);
72
+ const resolutionSelect = document.getElementById('resolution');
73
+
74
+ // Check if the saved value exists as an option
75
+ const optionExists = Array.from(resolutionSelect.options).some(opt => parseFloat(opt.value) === resolution);
76
+
77
+ if (!optionExists) {
78
+ // Add the custom resolution as an option
79
+ const newOption = document.createElement('option');
80
+ newOption.value = resolution.toFixed(3);
81
+ newOption.textContent = `${resolution.toFixed(3)}mm`;
82
+ resolutionSelect.appendChild(newOption);
83
+ }
84
+
85
+ // Set the value as a string to match the option values
86
+ resolutionSelect.value = resolution.toFixed(3);
87
+ }
88
+
89
+ const savedZFloor = localStorage.getItem('raster-zFloor');
90
+ if (savedZFloor !== null) {
91
+ zFloor = parseFloat(savedZFloor);
92
+ document.getElementById('z-floor').value = zFloor;
93
+ }
94
+
95
+ const savedXStep = localStorage.getItem('raster-xStep');
96
+ if (savedXStep !== null) {
97
+ xStep = parseInt(savedXStep);
98
+ document.getElementById('x-step').value = xStep;
99
+ }
100
+
101
+ const savedYStep = localStorage.getItem('raster-yStep');
102
+ if (savedYStep !== null) {
103
+ yStep = parseInt(savedYStep);
104
+ document.getElementById('y-step').value = yStep;
105
+ }
106
+
107
+ const savedAngleStep = localStorage.getItem('raster-angleStep');
108
+ if (savedAngleStep !== null) {
109
+ angleStep = parseFloat(savedAngleStep);
110
+ document.getElementById('angle-step').value = angleStep;
111
+ }
112
+
113
+ // Restore view checkboxes
114
+ const savedShowWrapped = localStorage.getItem('raster-showWrapped');
115
+ if (savedShowWrapped !== null) {
116
+ const showWrappedCheckbox = document.getElementById('show-wrapped');
117
+ if (showWrappedCheckbox) {
118
+ showWrappedCheckbox.checked = savedShowWrapped === 'true';
119
+ }
120
+ }
121
+ }
122
+
123
+ // ============================================================================
124
+ // IndexedDB for STL Caching
125
+ // ============================================================================
126
+
127
+ const DB_NAME = 'raster-path-cache';
128
+ const DB_VERSION = 1;
129
+ const STORE_NAME = 'stl-files';
130
+
131
+ async function openDB() {
132
+ return new Promise((resolve, reject) => {
133
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
134
+
135
+ request.onerror = () => reject(request.error);
136
+ request.onsuccess = () => resolve(request.result);
137
+
138
+ request.onupgradeneeded = (event) => {
139
+ const db = event.target.result;
140
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
141
+ db.createObjectStore(STORE_NAME);
142
+ }
143
+ };
144
+ });
145
+ }
146
+
147
+ async function cacheSTL(key, arrayBuffer, name) {
148
+ const db = await openDB();
149
+ return new Promise((resolve, reject) => {
150
+ const transaction = db.transaction(STORE_NAME, 'readwrite');
151
+ const store = transaction.objectStore(STORE_NAME);
152
+ const data = { arrayBuffer, name };
153
+ const request = store.put(data, key);
154
+ request.onsuccess = () => resolve();
155
+ request.onerror = () => reject(request.error);
156
+ });
157
+ }
158
+
159
+ async function getCachedSTL(key) {
160
+ const db = await openDB();
161
+ return new Promise((resolve, reject) => {
162
+ const transaction = db.transaction(STORE_NAME, 'readonly');
163
+ const store = transaction.objectStore(STORE_NAME);
164
+ const request = store.get(key);
165
+ request.onsuccess = () => resolve(request.result);
166
+ request.onerror = () => reject(request.error);
167
+ });
168
+ }
169
+
170
+ // ============================================================================
171
+ // STL Parsing
172
+ // ============================================================================
173
+
174
+ function calculateTriangleBounds(triangles) {
175
+ const bounds = {
176
+ min: { x: Infinity, y: Infinity, z: Infinity },
177
+ max: { x: -Infinity, y: -Infinity, z: -Infinity }
178
+ };
179
+ for (let i = 0; i < triangles.length; i += 3) {
180
+ bounds.min.x = Math.min(bounds.min.x, triangles[i]);
181
+ bounds.max.x = Math.max(bounds.max.x, triangles[i]);
182
+ bounds.min.y = Math.min(bounds.min.y, triangles[i + 1]);
183
+ bounds.max.y = Math.max(bounds.max.y, triangles[i + 1]);
184
+ bounds.min.z = Math.min(bounds.min.z, triangles[i + 2]);
185
+ bounds.max.z = Math.max(bounds.max.z, triangles[i + 2]);
186
+ }
187
+ return bounds;
188
+ }
189
+
190
+ function parseSTL(arrayBuffer) {
191
+ const view = new DataView(arrayBuffer);
192
+
193
+ // Check if ASCII (starts with "solid")
194
+ const text = new TextDecoder().decode(arrayBuffer.slice(0, 80));
195
+ if (text.toLowerCase().startsWith('solid')) {
196
+ return parseASCIISTL(arrayBuffer);
197
+ } else {
198
+ return parseBinarySTL(view);
199
+ }
200
+ }
201
+
202
+ function parseBinarySTL(view) {
203
+ const numTriangles = view.getUint32(80, true);
204
+ const triangles = new Float32Array(numTriangles * 9);
205
+
206
+ let offset = 84;
207
+ let floatIndex = 0;
208
+
209
+ for (let i = 0; i < numTriangles; i++) {
210
+ offset += 12; // Skip normal
211
+
212
+ for (let j = 0; j < 9; j++) {
213
+ triangles[floatIndex++] = view.getFloat32(offset, true);
214
+ offset += 4;
215
+ }
216
+
217
+ offset += 2; // Skip attribute byte count
218
+ }
219
+
220
+ return triangles;
221
+ }
222
+
223
+ function parseASCIISTL(arrayBuffer) {
224
+ const text = new TextDecoder().decode(arrayBuffer);
225
+ const lines = text.split('\n');
226
+ const triangles = [];
227
+ let vertices = [];
228
+
229
+ for (const line of lines) {
230
+ const trimmed = line.trim();
231
+ if (trimmed.startsWith('vertex')) {
232
+ const parts = trimmed.split(/\s+/);
233
+ vertices.push(
234
+ parseFloat(parts[1]),
235
+ parseFloat(parts[2]),
236
+ parseFloat(parts[3])
237
+ );
238
+
239
+ if (vertices.length === 9) {
240
+ triangles.push(...vertices);
241
+ vertices = [];
242
+ }
243
+ }
244
+ }
245
+
246
+ return new Float32Array(triangles);
247
+ }
248
+
249
+ // ============================================================================
250
+ // File Loading
251
+ // ============================================================================
252
+
253
+ async function loadSTLFile(isModel) {
254
+ const input = document.createElement('input');
255
+ input.type = 'file';
256
+ input.accept = '.stl';
257
+
258
+ return new Promise((resolve) => {
259
+ input.onchange = async (e) => {
260
+ const file = e.target.files[0];
261
+ if (!file) return resolve(null);
262
+
263
+ const arrayBuffer = await file.arrayBuffer();
264
+ const triangles = parseSTL(arrayBuffer);
265
+
266
+ // Cache in IndexedDB with filename
267
+ const cacheKey = isModel ? 'model-stl' : 'tool-stl';
268
+ await cacheSTL(cacheKey, arrayBuffer, file.name);
269
+
270
+ updateInfo(`Loaded ${file.name}: ${(triangles.length / 9).toLocaleString()} triangles`);
271
+ resolve({ arrayBuffer, triangles, name: file.name });
272
+ };
273
+
274
+ input.click();
275
+ });
276
+ }
277
+
278
+ // ============================================================================
279
+ // RasterPath Integration
280
+ // ============================================================================
281
+
282
+ async function initRasterPath() {
283
+ if (rasterPath) {
284
+ rasterPath.terminate();
285
+ }
286
+
287
+ rasterPath = new RasterPath({
288
+ mode: mode,
289
+ resolution: resolution,
290
+ rotationStep: mode === 'radial' ? angleStep : undefined,
291
+ // trianglesPerTile: 15000,
292
+ debug: true // Enable debug logging
293
+ });
294
+
295
+ await rasterPath.init();
296
+ updateInfo(`RasterPath initialized: ${mode} mode, ${resolution}mm resolution`);
297
+ }
298
+
299
+ async function rasterizeAll() {
300
+ if (!modelTriangles && !toolTriangles) {
301
+ updateInfo('No STL files loaded');
302
+ return;
303
+ }
304
+
305
+ try {
306
+ // Ensure RasterPath is initialized with current settings
307
+ await initRasterPath();
308
+
309
+ // Load tool (works for both modes)
310
+ if (toolTriangles) {
311
+ updateInfo('Loading tool...');
312
+ const t0 = performance.now();
313
+ toolRasterData = await rasterPath.loadTool({
314
+ triangles: toolTriangles
315
+ });
316
+ const t1 = performance.now();
317
+ updateInfo(`Tool loaded in ${(t1 - t0).toFixed(0)}ms`);
318
+ }
319
+
320
+ if (mode === 'planar') {
321
+ // Planar mode: rasterize terrain immediately
322
+ if (modelTriangles) {
323
+ updateInfo('Rasterizing terrain...');
324
+ const t0 = performance.now();
325
+ modelRasterData = await rasterPath.loadTerrain({
326
+ triangles: modelTriangles,
327
+ zFloor: zFloor
328
+ });
329
+ const t1 = performance.now();
330
+ updateInfo(`Terrain rasterized in ${(t1 - t0).toFixed(0)}ms`);
331
+ }
332
+ } else {
333
+ // Radial mode: MUST load tool FIRST
334
+ if (!toolTriangles) {
335
+ updateInfo('Error: Radial mode requires tool to be loaded first');
336
+ return;
337
+ }
338
+
339
+ // Load terrain (stores triangles for later, doesn't rasterize yet)
340
+ if (modelTriangles) {
341
+ updateInfo('Loading terrain...');
342
+ const t0 = performance.now();
343
+ await rasterPath.loadTerrain({
344
+ triangles: modelTriangles,
345
+ zFloor: zFloor
346
+ });
347
+ const t1 = performance.now();
348
+ updateInfo(`Terrain loaded in ${(t1 - t0).toFixed(0)}ms (will rasterize during toolpath generation)`);
349
+ } else {
350
+ updateInfo('Tool loaded. Load model and click "Generate Toolpath" to continue.');
351
+ }
352
+
353
+ // Update button states for radial mode
354
+ updateButtonStates();
355
+ return; // Don't rasterize model here - do it in toolpath generation
356
+ }
357
+
358
+ updateInfo('Rasterization complete');
359
+
360
+ // Auto-enable raster view
361
+ document.getElementById('show-raster').checked = true;
362
+
363
+ updateVisualization();
364
+ updateButtonStates();
365
+
366
+ } catch (error) {
367
+ console.error('Rasterization error:', error);
368
+ updateInfo(`Error: ${error.message}`);
369
+ }
370
+ }
371
+
372
+ async function generateToolpath() {
373
+ // Check if tool and terrain are loaded (unified check for both modes)
374
+ if (!toolRasterData) {
375
+ updateInfo('Tool must be loaded first');
376
+ return;
377
+ }
378
+
379
+ if (mode === 'planar') {
380
+ if (!modelRasterData) {
381
+ updateInfo('Model must be rasterized first');
382
+ return;
383
+ }
384
+ } else {
385
+ // Radial mode: terrain must be loaded (stored internally)
386
+ if (!modelTriangles) {
387
+ updateInfo('Model STL must be loaded');
388
+ return;
389
+ }
390
+ }
391
+
392
+ try {
393
+ const t0 = performance.now();
394
+ updateInfo('Generating toolpath...');
395
+
396
+ // Unified API - works for both modes!
397
+ toolpathData = await rasterPath.generateToolpaths({
398
+ xStep: xStep,
399
+ yStep: yStep,
400
+ zFloor: zFloor
401
+ });
402
+
403
+ const t1 = performance.now();
404
+
405
+ if (mode === 'planar') {
406
+ const numPoints = toolpathData.pathData.length;
407
+ updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
408
+ } else {
409
+ console.log('Radial toolpaths generated:', toolpathData);
410
+ console.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
411
+ updateInfo(`Toolpath generated: ${toolpathData.numStrips} strips, ${toolpathData.totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
412
+
413
+ // Store terrain strips for visualization
414
+ if (toolpathData.terrainStrips) {
415
+ modelRasterData = toolpathData.terrainStrips;
416
+ console.log('[Radial] Terrain strips available:', modelRasterData.length, 'strips');
417
+
418
+ // DEBUG: Check X range in strips
419
+ if (modelRasterData.length > 0) {
420
+ const strip0 = modelRasterData[0];
421
+ console.log('[Radial] Strip 0 structure:', {
422
+ angle: strip0.angle,
423
+ pointCount: strip0.pointCount,
424
+ positionsLength: strip0.positions?.length,
425
+ gridWidth: strip0.gridWidth,
426
+ gridHeight: strip0.gridHeight,
427
+ bounds: strip0.bounds
428
+ });
429
+
430
+ // Find actual X range in positions
431
+ if (strip0.positions && strip0.positions.length > 0) {
432
+ let minX = Infinity, maxX = -Infinity;
433
+ for (let i = 0; i < strip0.positions.length; i += 3) {
434
+ const x = strip0.positions[i];
435
+ minX = Math.min(minX, x);
436
+ maxX = Math.max(maxX, x);
437
+ }
438
+ console.log('[Radial] Strip 0 actual X range in positions:', minX.toFixed(2), 'to', maxX.toFixed(2));
439
+ }
440
+ }
441
+ } else if (toolpathData.strips && toolpathData.strips[0]?.terrainBounds) {
442
+ // Batched mode: create synthetic terrain strips with bounds only
443
+ modelRasterData = toolpathData.strips.map(strip => ({
444
+ angle: strip.angle,
445
+ bounds: strip.terrainBounds
446
+ }));
447
+ console.log('[Radial] Batched mode: created synthetic terrain strips from bounds:', modelRasterData.length, 'strips');
448
+ }
449
+ // This is fine - we only need toolpathData for visualization
450
+ }
451
+
452
+ // Auto-enable toolpath view
453
+ document.getElementById('show-paths').checked = true;
454
+
455
+ updateVisualization();
456
+
457
+ } catch (error) {
458
+ console.error('Toolpath generation error:', error);
459
+ updateInfo(`Error: ${error.message}`);
460
+ }
461
+ }
462
+
463
+ // ============================================================================
464
+ // Three.js Visualization
465
+ // ============================================================================
466
+
467
+ function initThreeJS() {
468
+ // Scene
469
+ scene = new THREE.Scene();
470
+ scene.background = new THREE.Color(0x1a1a1a);
471
+
472
+ // Camera
473
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
474
+ camera.position.set(100, 100, 100);
475
+ camera.lookAt(0, 0, 0);
476
+
477
+ // Renderer
478
+ const canvas = document.getElementById('canvas');
479
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
480
+ renderer.setSize(window.innerWidth, window.innerHeight);
481
+ renderer.setPixelRatio(window.devicePixelRatio);
482
+
483
+ // Controls
484
+ controls = new OrbitControls(camera, canvas);
485
+
486
+ // Lights
487
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
488
+ scene.add(ambientLight);
489
+
490
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
491
+ directionalLight.position.set(100, 100, 100);
492
+ scene.add(directionalLight);
493
+
494
+ // Grid
495
+ const gridHelper = new THREE.GridHelper(200, 20, 0x444444, 0x222222);
496
+ scene.add(gridHelper);
497
+
498
+ // Create rotated group for all visualizations (-90deg around X)
499
+ rotatedGroup = new THREE.Group();
500
+ rotatedGroup.rotation.x = -Math.PI / 2;
501
+ scene.add(rotatedGroup);
502
+
503
+ // Axes
504
+ const axesHelper = new THREE.AxesHelper(50);
505
+ scene.add(axesHelper);
506
+
507
+ // Axis labels
508
+ function makeTextSprite(message, color) {
509
+ const canvas = document.createElement('canvas');
510
+ const context = canvas.getContext('2d');
511
+ canvas.width = 256;
512
+ canvas.height = 128;
513
+ context.font = 'Bold 48px Arial';
514
+ context.fillStyle = color;
515
+ context.textAlign = 'center';
516
+ context.fillText(message, 128, 64);
517
+
518
+ const texture = new THREE.CanvasTexture(canvas);
519
+ const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
520
+ const sprite = new THREE.Sprite(spriteMaterial);
521
+ sprite.scale.set(10, 5, 1);
522
+ return sprite;
523
+ }
524
+
525
+ const xLabel = makeTextSprite('X', '#ff0000');
526
+ xLabel.position.set(60, 0, 0);
527
+ scene.add(xLabel);
528
+
529
+ const yLabel = makeTextSprite('Y', '#00ff00');
530
+ yLabel.position.set(0, 0, -60);
531
+ scene.add(yLabel);
532
+
533
+ const zLabel = makeTextSprite('Z', '#0000ff');
534
+ zLabel.position.set(0, 60, 6);
535
+ scene.add(zLabel);
536
+
537
+ // Window resize
538
+ window.addEventListener('resize', () => {
539
+ camera.aspect = window.innerWidth / window.innerHeight;
540
+ camera.updateProjectionMatrix();
541
+ renderer.setSize(window.innerWidth, window.innerHeight);
542
+ });
543
+
544
+ // Animation loop
545
+ function animate() {
546
+ requestAnimationFrame(animate);
547
+ controls.update();
548
+ renderer.render(scene, camera);
549
+ }
550
+ animate();
551
+ }
552
+
553
+ function updateVisualization() {
554
+ const showModel = document.getElementById('show-model').checked;
555
+ const showRaster = document.getElementById('show-raster').checked;
556
+ const showPaths = document.getElementById('show-paths').checked;
557
+ const showWrapped = document.getElementById('show-wrapped').checked;
558
+
559
+ // Model mesh
560
+ if (modelMesh) {
561
+ modelMesh.visible = showModel;
562
+ } else if (showModel && modelTriangles) {
563
+ displayModelMesh();
564
+ }
565
+
566
+ // Tool mesh
567
+ if (toolMesh) {
568
+ toolMesh.visible = showModel;
569
+ } else if (showModel && toolTriangles) {
570
+ displayToolMesh();
571
+ }
572
+
573
+ // Model raster points
574
+ if (modelRasterPoints) {
575
+ rotatedGroup.remove(modelRasterPoints);
576
+ modelRasterPoints.geometry.dispose();
577
+ modelRasterPoints.material.dispose();
578
+ modelRasterPoints = null;
579
+ }
580
+
581
+ if (showRaster && modelRasterData) {
582
+ displayModelRaster(showWrapped);
583
+ }
584
+
585
+ // Tool raster points
586
+ if (toolRasterPoints) {
587
+ rotatedGroup.remove(toolRasterPoints);
588
+ toolRasterPoints.geometry.dispose();
589
+ toolRasterPoints.material.dispose();
590
+ toolRasterPoints = null;
591
+ }
592
+
593
+ if (showRaster && toolRasterData) {
594
+ displayToolRaster();
595
+ }
596
+
597
+ // Toolpath points
598
+ if (toolpathPoints) {
599
+ rotatedGroup.remove(toolpathPoints);
600
+ toolpathPoints.geometry.dispose();
601
+ toolpathPoints.material.dispose();
602
+ toolpathPoints = null;
603
+ }
604
+
605
+ if (showPaths && toolpathData) {
606
+ displayToolpaths(showWrapped);
607
+ }
608
+ }
609
+
610
+ function displayModelMesh() {
611
+ if (!modelTriangles) return;
612
+
613
+ const geometry = new THREE.BufferGeometry();
614
+ geometry.setAttribute('position', new THREE.BufferAttribute(modelTriangles, 3));
615
+ geometry.computeVertexNormals();
616
+ geometry.computeBoundingBox();
617
+
618
+ const material = new THREE.MeshPhongMaterial({
619
+ color: 0x00ffff,
620
+ shininess: 30,
621
+ transparent: true,
622
+ opacity: 0.6
623
+ });
624
+
625
+ modelMesh = new THREE.Mesh(geometry, material);
626
+ rotatedGroup.add(modelMesh);
627
+
628
+ // Calculate max Z for tool offset
629
+ modelMaxZ = geometry.boundingBox.max.z + 20;
630
+
631
+ // update tool position when terrain (re)loaded
632
+ toolMesh && (toolMesh.position.z = modelMaxZ); // Offset tool above model
633
+ }
634
+
635
+ function displayToolMesh() {
636
+ if (!toolTriangles) return;
637
+
638
+ const geometry = new THREE.BufferGeometry();
639
+ geometry.setAttribute('position', new THREE.BufferAttribute(toolTriangles, 3));
640
+ geometry.computeVertexNormals();
641
+
642
+ const material = new THREE.MeshPhongMaterial({
643
+ color: 0xff6400, // Orange color for tool
644
+ shininess: 30,
645
+ side: THREE.DoubleSide,
646
+ transparent: true,
647
+ opacity: 0.8
648
+ });
649
+
650
+ toolMesh = new THREE.Mesh(geometry, material);
651
+ toolMesh.position.z = modelMaxZ; // Offset tool above model
652
+ rotatedGroup.add(toolMesh);
653
+ }
654
+
655
+ function displayModelRaster(wrapped) {
656
+ if (!modelRasterData) return;
657
+
658
+ const positions = [];
659
+ const colors = [];
660
+
661
+ if (mode === 'planar') {
662
+ // Planar: terrain is dense (Z-only array)
663
+ const { positions: rasterPos, bounds, gridWidth, gridHeight } = modelRasterData;
664
+ const stepSize = resolution;
665
+
666
+ for (let gy = 0; gy < gridHeight; gy++) {
667
+ for (let gx = 0; gx < gridWidth; gx++) {
668
+ const idx = gy * gridWidth + gx;
669
+ const z = rasterPos[idx];
670
+
671
+ if (z > -1e9) {
672
+ const x = bounds.min.x + gx * stepSize;
673
+ const y = bounds.min.y + gy * stepSize;
674
+
675
+ positions.push(x, y, z);
676
+ colors.push(0, 1, 0); // Green
677
+ }
678
+ }
679
+ }
680
+
681
+ } else {
682
+ // Radial: modelRasterData is an array of strips
683
+ // Each strip has: { angle, positions (sparse XYZ), gridWidth, gridHeight, bounds }
684
+ if (!Array.isArray(modelRasterData)) {
685
+ console.error('[Display] modelRasterData is not an array of strips');
686
+ return;
687
+ }
688
+
689
+ if (wrapped) {
690
+ // Wrap each strip around X-axis at its angle
691
+ console.log('[Display] Wrapping', modelRasterData.length, 'strips');
692
+
693
+ // Track overall X range for debug
694
+ let overallMinX = Infinity, overallMaxX = -Infinity;
695
+ let totalPointsRendered = 0;
696
+
697
+ for (const strip of modelRasterData) {
698
+ const { angle, positions: stripPositions } = strip;
699
+
700
+ if (!stripPositions || stripPositions.length === 0) continue;
701
+
702
+ const theta = angle * Math.PI / 180;
703
+ const cosTheta = Math.cos(theta);
704
+ const sinTheta = Math.sin(theta);
705
+
706
+ // Strip positions are sparse XYZ triplets
707
+ for (let i = 0; i < stripPositions.length; i += 3) {
708
+ const x = stripPositions[i];
709
+ const y_planar = stripPositions[i + 1]; // Y in planar coordinates
710
+ const terrainHeight = stripPositions[i + 2]; // Distance from X-axis
711
+
712
+ overallMinX = Math.min(overallMinX, x);
713
+ overallMaxX = Math.max(overallMaxX, x);
714
+
715
+ // Wrap: use terrainHeight as radial distance from X-axis
716
+ const y = terrainHeight * cosTheta;
717
+ const z = terrainHeight * sinTheta;
718
+
719
+ positions.push(x, y, z);
720
+ colors.push(0, 1, 0); // Green
721
+ totalPointsRendered++;
722
+ }
723
+ }
724
+
725
+ console.log('[Display] Rendered', totalPointsRendered, 'points from', modelRasterData.length, 'strips');
726
+ console.log('[Display] X range: [' + overallMinX.toFixed(2) + ', ' + overallMaxX.toFixed(2) + ']');
727
+
728
+ // DEBUG: Check angle coverage
729
+ 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(', '));
733
+ } else {
734
+ // Show unwrapped (planar) - lay out strips side by side
735
+ for (let stripIdx = 0; stripIdx < modelRasterData.length; stripIdx++) {
736
+ const strip = modelRasterData[stripIdx];
737
+ const { positions: stripPositions } = strip;
738
+
739
+ if (!stripPositions || stripPositions.length === 0) continue;
740
+
741
+ const stripY = stripIdx * resolution * 10; // Offset each strip for visibility
742
+
743
+ // Strip positions are sparse XYZ triplets
744
+ for (let i = 0; i < stripPositions.length; i += 3) {
745
+ const x = stripPositions[i];
746
+ const terrainHeight = stripPositions[i + 2];
747
+
748
+ positions.push(x, stripY, terrainHeight);
749
+ colors.push(0, 1, 0); // Green
750
+ }
751
+ }
752
+ }
753
+ }
754
+
755
+ if (positions.length > 0) {
756
+ const geometry = new THREE.BufferGeometry();
757
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
758
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
759
+
760
+ // Scale point size with resolution for proper spacing
761
+ const pointSize = resolution; // Leave some space between points
762
+
763
+ const material = new THREE.PointsMaterial({
764
+ size: pointSize,
765
+ vertexColors: true
766
+ });
767
+
768
+ modelRasterPoints = new THREE.Points(geometry, material);
769
+ rotatedGroup.add(modelRasterPoints);
770
+ }
771
+ }
772
+
773
+ function displayToolRaster() {
774
+ if (!toolRasterData) return;
775
+
776
+ const positions = [];
777
+ const colors = [];
778
+
779
+ // Tool raster is always sparse format [gridX, gridY, Z]
780
+ const { positions: rasterPos, bounds } = toolRasterData;
781
+ const stepSize = resolution;
782
+
783
+ for (let i = 0; i < rasterPos.length; i += 3) {
784
+ const gridX = rasterPos[i];
785
+ const gridY = rasterPos[i + 1];
786
+ const z = -rasterPos[i + 2];
787
+ const x = bounds.min.x + gridX * stepSize;
788
+ const y = bounds.min.y + gridY * stepSize;
789
+
790
+ positions.push(x, y, z);
791
+ colors.push(1, 0.4, 0);
792
+ }
793
+
794
+ if (positions.length > 0) {
795
+ const geometry = new THREE.BufferGeometry();
796
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
797
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
798
+
799
+ // Scale point size with resolution for proper spacing
800
+ const pointSize = resolution;
801
+ const material = new THREE.PointsMaterial({
802
+ size: pointSize,
803
+ vertexColors: true
804
+ });
805
+
806
+ toolRasterPoints = new THREE.Points(geometry, material);
807
+ toolRasterPoints.position.z = modelMaxZ; // Offset tool above model
808
+ rotatedGroup.add(toolRasterPoints);
809
+ }
810
+ }
811
+
812
+ function displayToolpaths(wrapped) {
813
+ if (!toolpathData) return;
814
+
815
+ if (mode === 'planar') {
816
+ // Planar toolpaths
817
+ const { pathData, numScanlines, pointsPerLine } = toolpathData;
818
+ const bounds = toolpathData.generationBounds || modelRasterData.bounds;
819
+ const stepSize = resolution;
820
+
821
+ const totalPoints = numScanlines * pointsPerLine;
822
+ const MAX_DISPLAY_POINTS = 10000000; // 10M points max for display
823
+
824
+ // Check if we need to downsample
825
+ 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`);
829
+ return;
830
+ }
831
+
832
+ // Preallocate typed arrays for better performance
833
+ const positions = new Float32Array(totalPoints * 3);
834
+ const colors = new Float32Array(totalPoints * 3);
835
+
836
+ let arrayIdx = 0;
837
+ for (let line = 0; line < numScanlines; line++) {
838
+ for (let pt = 0; pt < pointsPerLine; pt++) {
839
+ const idx = line * pointsPerLine + pt;
840
+ const z = pathData[idx];
841
+
842
+ const x = bounds.min.x + pt * xStep * stepSize;
843
+ const y = bounds.min.y + line * yStep * stepSize;
844
+
845
+ positions[arrayIdx] = x;
846
+ positions[arrayIdx + 1] = y;
847
+ positions[arrayIdx + 2] = z;
848
+
849
+ colors[arrayIdx] = 1; // R
850
+ colors[arrayIdx + 1] = 0.4; // G
851
+ colors[arrayIdx + 2] = 0; // B
852
+
853
+ arrayIdx += 3;
854
+ }
855
+ }
856
+
857
+ // Create geometry from preallocated arrays
858
+ const geometry = new THREE.BufferGeometry();
859
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
860
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
861
+
862
+ const material = new THREE.PointsMaterial({
863
+ size: resolution * 0.5,
864
+ vertexColors: true
865
+ });
866
+
867
+ toolpathPoints = new THREE.Points(geometry, material);
868
+ rotatedGroup.add(toolpathPoints);
869
+
870
+ return; // Exit early for planar mode
871
+
872
+ }
873
+
874
+ // Radial V2 toolpaths - each strip is independent
875
+ const { strips } = toolpathData;
876
+ const stepSize = resolution;
877
+
878
+ // Calculate total points across all strips
879
+ let totalPoints = 0;
880
+ for (const strip of strips) {
881
+ totalPoints += strip.numScanlines * strip.pointsPerLine;
882
+ }
883
+
884
+ const MAX_DISPLAY_POINTS = 10000000; // 10M points max for display
885
+
886
+ console.log('[Toolpath Display] Radial V2 mode:', strips.length, 'strips,', totalPoints, 'total points');
887
+
888
+ // Check if we need to skip visualization
889
+ 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`);
893
+ return;
894
+ }
895
+
896
+ // DEBUG: Check angle distribution AND data
897
+ if (strips.length > 0) {
898
+ 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(', '));
900
+ // Check if pathData is actually different between strips
901
+ if (strips.length > 360) {
902
+ const samples0 = strips[0].pathData.slice(0, 5).map(v => v.toFixed(3)).join(',');
903
+ 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)');
907
+ }
908
+ }
909
+
910
+ if (strips.length > 0) {
911
+ const firstStrip = strips[0];
912
+ // Note: modelRasterData may be null with new pipeline (rasterize + toolpath in one step)
913
+ const terrainStrip = modelRasterData ? modelRasterData.find(s => s.angle === firstStrip.angle) : null;
914
+
915
+ // Find min/max without stack overflow
916
+ let minVal = Infinity, maxVal = -Infinity;
917
+ for (let i = 0; i < firstStrip.pathData.length; i++) {
918
+ minVal = Math.min(minVal, firstStrip.pathData[i]);
919
+ maxVal = Math.max(maxVal, firstStrip.pathData[i]);
920
+ }
921
+
922
+ console.log('[Toolpath Display] First strip sample:', {
923
+ angle: firstStrip.angle,
924
+ numScanlines: firstStrip.numScanlines,
925
+ pointsPerLine: firstStrip.pointsPerLine,
926
+ firstValues: Array.from(firstStrip.pathData.slice(0, 10)),
927
+ minValue: minVal,
928
+ maxValue: maxVal,
929
+ terrainBounds: terrainStrip?.bounds
930
+ });
931
+ }
932
+
933
+ // Preallocate typed arrays for better performance
934
+ const positions = new Float32Array(totalPoints * 3);
935
+ const colors = new Float32Array(totalPoints * 3);
936
+ let arrayIdx = 0;
937
+
938
+ if (wrapped) {
939
+ // Wrap each strip around X-axis at its angle
940
+ // Each strip should have numScanlines=1 (single centerline)
941
+ for (const strip of strips) {
942
+ const { angle, pathData, numScanlines, pointsPerLine } = strip;
943
+ const terrainStrip = modelRasterData.find(s => s.angle === angle);
944
+ const stripBounds = terrainStrip?.bounds || { min: { x: -100, y: 0, z: 0 }, max: { x: 100, y: 10, z: 20 } };
945
+
946
+ // Rotate around X-axis at this angle
947
+ const theta = angle * Math.PI / 180;
948
+ const cosTheta = Math.cos(theta);
949
+ const sinTheta = Math.sin(theta);
950
+
951
+ // Should only be 1 scanline for radial
952
+ for (let line = 0; line < numScanlines; line++) {
953
+ for (let pt = 0; pt < pointsPerLine; pt++) {
954
+ const idx = line * pointsPerLine + pt;
955
+ const radius = pathData[idx]; // Tool tip radius from X-axis
956
+
957
+ const gridX = pt * xStep;
958
+ const x = stripBounds.min.x + gridX * stepSize;
959
+
960
+ // Wrap around X-axis
961
+ const yWrapped = radius * cosTheta;
962
+ const zWrapped = radius * sinTheta;
963
+
964
+ // Use direct indexing instead of push()
965
+ positions[arrayIdx] = x;
966
+ positions[arrayIdx + 1] = yWrapped;
967
+ positions[arrayIdx + 2] = zWrapped;
968
+
969
+ colors[arrayIdx] = 1; // R
970
+ colors[arrayIdx + 1] = 0.4; // G
971
+ colors[arrayIdx + 2] = 0; // B
972
+
973
+ arrayIdx += 3;
974
+ }
975
+ }
976
+ }
977
+ } else {
978
+ // Show unwrapped (not very useful for radial, but supported)
979
+ for (const strip of strips) {
980
+ const { angle, pathData, numScanlines, pointsPerLine } = strip;
981
+ // Get bounds from the strip's terrain data (or use default if not available)
982
+ const stripBounds = (modelRasterData && modelRasterData.find(s => s.angle === angle)?.bounds) ||
983
+ { min: { x: -100, y: 0, z: 0 }, max: { x: 100, y: 10, z: 20 } };
984
+ const yOffset = angle; // Use angle as Y offset for visualization
985
+
986
+ for (let line = 0; line < numScanlines; line++) {
987
+ const gridY = line * yStep;
988
+ const y = yOffset + gridY * stepSize;
989
+
990
+ for (let pt = 0; pt < pointsPerLine; pt++) {
991
+ const idx = line * pointsPerLine + pt;
992
+ const radius = pathData[idx];
993
+
994
+ const gridX = pt * xStep;
995
+ const x = stripBounds.min.x + gridX * stepSize;
996
+
997
+ // Use direct indexing instead of push()
998
+ positions[arrayIdx] = x;
999
+ positions[arrayIdx + 1] = y;
1000
+ positions[arrayIdx + 2] = radius;
1001
+
1002
+ colors[arrayIdx] = 1; // R
1003
+ colors[arrayIdx + 1] = 0.4; // G
1004
+ colors[arrayIdx + 2] = 0; // B
1005
+
1006
+ arrayIdx += 3;
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ if (positions.length > 0) {
1013
+ const geometry = new THREE.BufferGeometry();
1014
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
1015
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
1016
+
1017
+ // Scale point size with resolution
1018
+ const pointSize = resolution * 1.5; // Slightly larger than raster points
1019
+
1020
+ const material = new THREE.PointsMaterial({
1021
+ size: pointSize,
1022
+ vertexColors: true
1023
+ });
1024
+
1025
+ toolpathPoints = new THREE.Points(geometry, material);
1026
+
1027
+ // For wrapped radial mode, rotate 90° around X and center on model mesh
1028
+ if (wrapped) {
1029
+ toolpathPoints.rotation.x = Math.PI / 2;
1030
+
1031
+ // Center toolpath on model mesh
1032
+ if (modelMesh) {
1033
+ geometry.computeBoundingBox();
1034
+ const toolpathCenter = new THREE.Vector3();
1035
+ geometry.boundingBox.getCenter(toolpathCenter);
1036
+
1037
+ const modelCenter = new THREE.Vector3();
1038
+ modelMesh.geometry.boundingBox.getCenter(modelCenter);
1039
+
1040
+ // Offset toolpath to match model center
1041
+ toolpathPoints.position.copy(modelCenter).sub(toolpathCenter);
1042
+ }
1043
+ }
1044
+
1045
+ rotatedGroup.add(toolpathPoints);
1046
+ }
1047
+ }
1048
+
1049
+ // ============================================================================
1050
+ // UI Updates
1051
+ // ============================================================================
1052
+
1053
+ function updateInfo(text) {
1054
+ console.log(text);
1055
+ document.getElementById('info').textContent = text;
1056
+ }
1057
+
1058
+ function updateButtonStates() {
1059
+ const hasModel = modelTriangles !== null;
1060
+ const hasTool = toolTriangles !== null;
1061
+ const hasAnySTL = hasModel || hasTool;
1062
+
1063
+ // Different requirements for planar vs radial mode
1064
+ let canGenerateToolpath;
1065
+ if (mode === 'planar') {
1066
+ // Planar: need both model and tool rasterized
1067
+ canGenerateToolpath = modelRasterData !== null && toolRasterData !== null;
1068
+ } else {
1069
+ // Radial: only need tool rasterized (model is rasterized during toolpath gen)
1070
+ canGenerateToolpath = toolRasterData !== null && hasModel;
1071
+ }
1072
+
1073
+ document.getElementById('rasterize').disabled = !hasAnySTL;
1074
+ document.getElementById('generate-toolpath').disabled = !canGenerateToolpath;
1075
+ }
1076
+
1077
+ // ============================================================================
1078
+ // Event Handlers
1079
+ // ============================================================================
1080
+
1081
+ function updateModeUI() {
1082
+ // Show/hide wrapped toggle and angle step for radial mode
1083
+ const wrappedContainer = document.getElementById('wrapped-container').classList;
1084
+ const angleStepContainer = document.getElementById('angle-step-container').classList;
1085
+ if (mode === 'radial') {
1086
+ wrappedContainer.remove('hide');
1087
+ angleStepContainer.remove('hide');
1088
+ } else {
1089
+ wrappedContainer.add('hide');
1090
+ angleStepContainer.add('hide');
1091
+ }
1092
+ }
1093
+
1094
+ document.addEventListener('DOMContentLoaded', async () => {
1095
+ // Load saved parameters
1096
+ loadParameters();
1097
+
1098
+ // Initialize Three.js
1099
+ initThreeJS();
1100
+
1101
+ // Load cached STLs from IndexedDB
1102
+ const cachedModel = await getCachedSTL('model-stl');
1103
+ if (cachedModel) {
1104
+ // Handle both old format (raw ArrayBuffer) and new format (object with arrayBuffer and name)
1105
+ const isOldFormat = cachedModel instanceof ArrayBuffer;
1106
+ modelSTL = isOldFormat ? cachedModel : cachedModel.arrayBuffer;
1107
+ modelTriangles = parseSTL(modelSTL);
1108
+ document.getElementById('model-status').textContent = isOldFormat ? 'Cached model' : (cachedModel.name || 'Cached model');
1109
+ displayModelMesh();
1110
+ }
1111
+
1112
+ const cachedTool = await getCachedSTL('tool-stl');
1113
+ if (cachedTool) {
1114
+ // Handle both old format (raw ArrayBuffer) and new format (object with arrayBuffer and name)
1115
+ const isOldFormat = cachedTool instanceof ArrayBuffer;
1116
+ toolSTL = isOldFormat ? cachedTool : cachedTool.arrayBuffer;
1117
+ toolTriangles = parseSTL(toolSTL);
1118
+ document.getElementById('tool-status').textContent = isOldFormat ? 'Cached tool' : (cachedTool.name || 'Cached tool');
1119
+ displayToolMesh();
1120
+ }
1121
+
1122
+ updateButtonStates();
1123
+
1124
+ // Mode toggle
1125
+ document.querySelectorAll('input[name="mode"]').forEach(radio => {
1126
+ radio.addEventListener('change', (e) => {
1127
+ mode = e.target.value;
1128
+ updateModeUI();
1129
+
1130
+ // Clear raster data (needs re-rasterization with new mode)
1131
+ modelRasterData = null;
1132
+ toolRasterData = null;
1133
+ toolpathData = null;
1134
+
1135
+ saveParameters();
1136
+ updateInfo(`Mode changed to ${mode}`);
1137
+ updateButtonStates();
1138
+ updateVisualization();
1139
+ });
1140
+ });
1141
+
1142
+ // Resolution change
1143
+ document.getElementById('resolution').addEventListener('change', (e) => {
1144
+ resolution = parseFloat(e.target.value);
1145
+
1146
+ // Clear raster data (needs re-rasterization with new resolution)
1147
+ modelRasterData = null;
1148
+ toolRasterData = null;
1149
+ toolpathData = null;
1150
+
1151
+ saveParameters();
1152
+ updateInfo(`Resolution changed to ${resolution}mm`);
1153
+ updateButtonStates();
1154
+ });
1155
+
1156
+ // Z Floor change
1157
+ document.getElementById('z-floor').addEventListener('change', (e) => {
1158
+ zFloor = parseFloat(e.target.value);
1159
+
1160
+ // Clear raster data (needs re-rasterization with new zFloor)
1161
+ modelRasterData = null;
1162
+ toolRasterData = null;
1163
+ toolpathData = null;
1164
+
1165
+ saveParameters();
1166
+ updateInfo(`Z Floor changed to ${zFloor}`);
1167
+ updateButtonStates();
1168
+ });
1169
+
1170
+ // Stepping controls
1171
+ document.getElementById('x-step').addEventListener('change', (e) => {
1172
+ xStep = parseInt(e.target.value);
1173
+ toolpathData = null; // Need to regenerate toolpath
1174
+ saveParameters();
1175
+ updateInfo(`X Step changed to ${xStep}`);
1176
+ updateButtonStates();
1177
+ });
1178
+
1179
+ document.getElementById('y-step').addEventListener('change', (e) => {
1180
+ yStep = parseInt(e.target.value);
1181
+ toolpathData = null; // Need to regenerate toolpath
1182
+ saveParameters();
1183
+ updateInfo(`Y Step changed to ${yStep}`);
1184
+ updateButtonStates();
1185
+ });
1186
+
1187
+ document.getElementById('angle-step').addEventListener('change', (e) => {
1188
+ angleStep = parseFloat(e.target.value);
1189
+ if (mode === 'radial') {
1190
+ modelRasterData = null; // Need to re-rasterize with new angle step
1191
+ toolRasterData = null;
1192
+ toolpathData = null;
1193
+ }
1194
+ saveParameters();
1195
+ updateInfo(`Angle Step changed to ${angleStep}°`);
1196
+ updateButtonStates();
1197
+ });
1198
+
1199
+ // Load Model button
1200
+ document.getElementById('load-model').addEventListener('click', async () => {
1201
+ const result = await loadSTLFile(true);
1202
+ if (result) {
1203
+ modelSTL = result.arrayBuffer;
1204
+ modelTriangles = result.triangles;
1205
+ document.getElementById('model-status').textContent = result.name;
1206
+
1207
+ // Clear raster data
1208
+ modelRasterData = null;
1209
+ toolpathData = null;
1210
+
1211
+ displayModelMesh();
1212
+ updateButtonStates();
1213
+ }
1214
+ });
1215
+
1216
+ // Load Tool button
1217
+ document.getElementById('load-tool').addEventListener('click', async () => {
1218
+ const result = await loadSTLFile(false);
1219
+ if (result) {
1220
+ toolSTL = result.arrayBuffer;
1221
+ toolTriangles = result.triangles;
1222
+ document.getElementById('tool-status').textContent = result.name;
1223
+
1224
+ // Clear raster data
1225
+ toolRasterData = null;
1226
+ toolpathData = null;
1227
+
1228
+ displayToolMesh();
1229
+ updateButtonStates();
1230
+ }
1231
+ });
1232
+
1233
+ // Rasterize button
1234
+ document.getElementById('rasterize').addEventListener('click', rasterizeAll);
1235
+
1236
+ // Generate Toolpath button
1237
+ document.getElementById('generate-toolpath').addEventListener('click', generateToolpath);
1238
+
1239
+ // View toggles
1240
+ ['show-model', 'show-raster', 'show-paths', 'show-wrapped'].forEach(id => {
1241
+ const checkbox = document.getElementById(id);
1242
+ if (checkbox) {
1243
+ checkbox.addEventListener('change', () => {
1244
+ updateVisualization();
1245
+ // Save wrapped checkbox state
1246
+ if (id === 'show-wrapped') {
1247
+ saveParameters();
1248
+ }
1249
+ });
1250
+ }
1251
+ });
1252
+
1253
+ updateInfo('Ready - Load STL files to begin');
1254
+ });