@gridspace/raster-path 1.0.3 → 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/README.md +3 -5
- package/build/app.js +363 -39
- package/build/index.html +39 -1
- package/build/raster-path.js +13 -13
- package/build/style.css +65 -0
- package/build/webgpu-worker.js +475 -686
- package/package.json +6 -2
- package/scripts/build-shaders.js +1 -1
- package/src/index.js +13 -13
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/lathe-cylinder-2-debug.cjs +334 -0
- package/src/test/lathe-cylinder-2-test.cjs +157 -0
- package/src/test/lathe-cylinder-test.cjs +198 -0
- package/src/test/work-estimation-profile.cjs +406 -0
- package/src/test/workload-calculator-demo.cjs +113 -0
- package/src/test/workload-calibration.cjs +310 -0
- package/src/web/app.js +363 -39
- package/src/web/index.html +39 -1
- package/src/web/style.css +65 -0
- package/src/web/webgpu-worker.js +470 -687
- package/src/workload-calculator.js +318 -0
package/README.md
CHANGED
|
@@ -68,12 +68,11 @@ await raster.loadTerrain({
|
|
|
68
68
|
zFloor: 0
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
// Generate toolpaths
|
|
71
|
+
// Generate toolpaths
|
|
72
72
|
const toolpathData = await raster.generateToolpaths({
|
|
73
73
|
xStep: 5,
|
|
74
74
|
yStep: 5,
|
|
75
|
-
zFloor: 0
|
|
76
|
-
radiusOffset: 20 // Tool offset above surface (radial mode only)
|
|
75
|
+
zFloor: 0
|
|
77
76
|
});
|
|
78
77
|
|
|
79
78
|
// Output is array of strips (one per rotation angle)
|
|
@@ -222,14 +221,13 @@ await raster.loadTerrain({
|
|
|
222
221
|
|
|
223
222
|
---
|
|
224
223
|
|
|
225
|
-
#### `async generateToolpaths({ xStep, yStep, zFloor,
|
|
224
|
+
#### `async generateToolpaths({ xStep, yStep, zFloor, onProgress })`
|
|
226
225
|
Generate toolpaths from loaded tool and terrain. Must call `loadTool()` and `loadTerrain()` first.
|
|
227
226
|
|
|
228
227
|
**Parameters**:
|
|
229
228
|
- `xStep` (number): Sample every Nth point in X direction
|
|
230
229
|
- `yStep` (number): Sample every Nth point in Y direction
|
|
231
230
|
- `zFloor` (number): Z floor value for out-of-bounds areas
|
|
232
|
-
- `radiusOffset` (number, radial only): Tool offset above surface (mm)
|
|
233
231
|
- `onProgress` (function, optional): Progress callback `(progress: number) => void`
|
|
234
232
|
|
|
235
233
|
**Returns**:
|
package/build/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
|
-
|
|
292
|
-
debug: true
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
726
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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)
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
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;
|