@gridspace/raster-path 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/app.js +151 -12
- package/build/index.html +6 -2
- package/build/raster-path.js +118 -9
- package/build/raster-worker.js +421 -7
- package/package.json +4 -2
- package/src/core/path-radial.js +0 -1
- package/src/core/path-tracing.js +492 -0
- package/src/core/raster-config.js +13 -4
- package/src/core/raster-path.js +118 -9
- package/src/core/raster-worker.js +41 -0
- package/src/core/workload-calibrate.js +57 -3
- package/src/shaders/tracing-toolpath.wgsl +95 -0
- package/src/test/tracing-test.cjs +307 -0
- package/src/web/app.js +151 -12
- package/src/web/index.html +6 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// tracing-test.cjs
|
|
2
|
+
// Test for tracing mode using new RasterPath API
|
|
3
|
+
// Tests: loadTool() + loadTerrain() + generateToolpaths() with input paths
|
|
4
|
+
|
|
5
|
+
const { app, BrowserWindow } = require('electron');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
const OUTPUT_DIR = path.join(__dirname, '../../test-output');
|
|
10
|
+
const CURRENT_FILE = path.join(OUTPUT_DIR, 'tracing-current.json');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
13
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let mainWindow;
|
|
17
|
+
|
|
18
|
+
function createWindow() {
|
|
19
|
+
mainWindow = new BrowserWindow({
|
|
20
|
+
width: 1200,
|
|
21
|
+
height: 800,
|
|
22
|
+
show: false,
|
|
23
|
+
webPreferences: {
|
|
24
|
+
nodeIntegration: false,
|
|
25
|
+
contextIsolation: true,
|
|
26
|
+
enableBlinkFeatures: 'WebGPU',
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
31
|
+
mainWindow.loadFile(htmlPath);
|
|
32
|
+
|
|
33
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
34
|
+
console.log('✓ Page loaded');
|
|
35
|
+
|
|
36
|
+
const testScript = `
|
|
37
|
+
(async function() {
|
|
38
|
+
console.log('=== Tracing Mode Test ===');
|
|
39
|
+
|
|
40
|
+
if (!navigator.gpu) {
|
|
41
|
+
return { error: 'WebGPU not available' };
|
|
42
|
+
}
|
|
43
|
+
console.log('✓ WebGPU available');
|
|
44
|
+
|
|
45
|
+
// Import RasterPath
|
|
46
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
47
|
+
|
|
48
|
+
// Load STL files
|
|
49
|
+
console.log('\\nLoading STL files...');
|
|
50
|
+
const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
|
|
51
|
+
const terrainBuffer = await terrainResponse.arrayBuffer();
|
|
52
|
+
|
|
53
|
+
const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
|
|
54
|
+
const toolBuffer = await toolResponse.arrayBuffer();
|
|
55
|
+
|
|
56
|
+
console.log('✓ Loaded terrain.stl:', terrainBuffer.byteLength, 'bytes');
|
|
57
|
+
console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
|
|
58
|
+
|
|
59
|
+
// Parse STL files
|
|
60
|
+
function parseBinarySTL(buffer) {
|
|
61
|
+
const dataView = new DataView(buffer);
|
|
62
|
+
const numTriangles = dataView.getUint32(80, true);
|
|
63
|
+
const positions = new Float32Array(numTriangles * 9);
|
|
64
|
+
let offset = 84;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < numTriangles; i++) {
|
|
67
|
+
offset += 12; // Skip normal
|
|
68
|
+
for (let j = 0; j < 9; j++) {
|
|
69
|
+
positions[i * 9 + j] = dataView.getFloat32(offset, true);
|
|
70
|
+
offset += 4;
|
|
71
|
+
}
|
|
72
|
+
offset += 2; // Skip attribute byte count
|
|
73
|
+
}
|
|
74
|
+
return positions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const terrainTriangles = parseBinarySTL(terrainBuffer);
|
|
78
|
+
const toolTriangles = parseBinarySTL(toolBuffer);
|
|
79
|
+
console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
|
|
80
|
+
console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
|
|
81
|
+
|
|
82
|
+
// Calculate terrain bounds for path generation
|
|
83
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
84
|
+
for (let i = 0; i < terrainTriangles.length; i += 3) {
|
|
85
|
+
const x = terrainTriangles[i];
|
|
86
|
+
const y = terrainTriangles[i + 1];
|
|
87
|
+
minX = Math.min(minX, x);
|
|
88
|
+
maxX = Math.max(maxX, x);
|
|
89
|
+
minY = Math.min(minY, y);
|
|
90
|
+
maxY = Math.max(maxY, y);
|
|
91
|
+
}
|
|
92
|
+
console.log('✓ Terrain bounds:',
|
|
93
|
+
'X:', minX.toFixed(2), 'to', maxX.toFixed(2),
|
|
94
|
+
'Y:', minY.toFixed(2), 'to', maxY.toFixed(2));
|
|
95
|
+
|
|
96
|
+
// Generate a simple horizontal scanline across the terrain center
|
|
97
|
+
const centerY = (minY + maxY) / 2;
|
|
98
|
+
const numSegments = 10; // Sparse to test densification
|
|
99
|
+
const path1 = new Float32Array(numSegments * 2);
|
|
100
|
+
|
|
101
|
+
console.log('\\nGenerating horizontal scanline test path...');
|
|
102
|
+
console.log(' Y (fixed):', centerY.toFixed(2), 'mm');
|
|
103
|
+
console.log(' X range:', minX.toFixed(2), 'to', maxX.toFixed(2), 'mm');
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < numSegments; i++) {
|
|
106
|
+
const t = i / (numSegments - 1);
|
|
107
|
+
path1[i * 2] = minX + t * (maxX - minX); // X varies
|
|
108
|
+
path1[i * 2 + 1] = centerY; // Y constant
|
|
109
|
+
}
|
|
110
|
+
console.log('✓ Generated path with', numSegments, 'vertices');
|
|
111
|
+
|
|
112
|
+
// Test parameters
|
|
113
|
+
const resolution = 0.1; // 0.1mm resolution for terrain raster
|
|
114
|
+
const step = 0.5; // 0.5mm sampling resolution along path
|
|
115
|
+
const zFloor = -100;
|
|
116
|
+
|
|
117
|
+
console.log('\\nTest parameters:');
|
|
118
|
+
console.log(' Terrain resolution:', resolution, 'mm');
|
|
119
|
+
console.log(' Path sampling step:', step, 'mm');
|
|
120
|
+
console.log(' Z floor:', zFloor, 'mm');
|
|
121
|
+
|
|
122
|
+
// Create RasterPath instance for tracing mode
|
|
123
|
+
console.log('\\nInitializing RasterPath (tracing mode)...');
|
|
124
|
+
const raster = new RasterPath({
|
|
125
|
+
mode: 'tracing',
|
|
126
|
+
resolution: resolution
|
|
127
|
+
});
|
|
128
|
+
await raster.init();
|
|
129
|
+
console.log('✓ RasterPath initialized');
|
|
130
|
+
|
|
131
|
+
// Load tool
|
|
132
|
+
console.log('\\nLoading tool...');
|
|
133
|
+
const toolStartTime = performance.now();
|
|
134
|
+
await raster.loadTool({ triangles: toolTriangles });
|
|
135
|
+
const toolTime = performance.now() - toolStartTime;
|
|
136
|
+
console.log('✓ Tool loaded in', toolTime.toFixed(1), 'ms');
|
|
137
|
+
|
|
138
|
+
// Load terrain
|
|
139
|
+
console.log('\\nLoading terrain...');
|
|
140
|
+
const terrainStartTime = performance.now();
|
|
141
|
+
const terrainData = await raster.loadTerrain({
|
|
142
|
+
triangles: terrainTriangles,
|
|
143
|
+
zFloor: zFloor
|
|
144
|
+
});
|
|
145
|
+
const terrainTime = performance.now() - terrainStartTime;
|
|
146
|
+
console.log('✓ Terrain loaded in', terrainTime.toFixed(1), 'ms');
|
|
147
|
+
console.log(' Grid:', terrainData.width, 'x', terrainData.height);
|
|
148
|
+
|
|
149
|
+
// Check terrain data has actual values
|
|
150
|
+
const terrainSamples = [];
|
|
151
|
+
for (let i = 0; i < Math.min(10, terrainData.positions.length); i++) {
|
|
152
|
+
terrainSamples.push(terrainData.positions[i].toFixed(3));
|
|
153
|
+
}
|
|
154
|
+
console.log(' Terrain Z samples:', terrainSamples.join(', '));
|
|
155
|
+
const nonEmpty = terrainData.positions.filter(z => z > -1e9).length;
|
|
156
|
+
console.log(' Non-empty terrain cells:', nonEmpty, '/', terrainData.positions.length,
|
|
157
|
+
'(' + (100 * nonEmpty / terrainData.positions.length).toFixed(1) + '%)');
|
|
158
|
+
|
|
159
|
+
// Create reusable buffers for optimal iterative tracing
|
|
160
|
+
console.log('\\nCreating reusable tracing buffers...');
|
|
161
|
+
const buffersStartTime = performance.now();
|
|
162
|
+
await raster.createTracingBuffers();
|
|
163
|
+
const buffersTime = performance.now() - buffersStartTime;
|
|
164
|
+
console.log('✓ Reusable buffers created in', buffersTime.toFixed(1), 'ms');
|
|
165
|
+
|
|
166
|
+
// Generate traced toolpaths
|
|
167
|
+
console.log('\\nGenerating traced toolpaths...');
|
|
168
|
+
const toolpathStartTime = performance.now();
|
|
169
|
+
const result = await raster.generateToolpaths({
|
|
170
|
+
paths: [path1],
|
|
171
|
+
step: step,
|
|
172
|
+
zFloor: zFloor,
|
|
173
|
+
onProgress: (percent, info) => {
|
|
174
|
+
console.log(' Progress:', percent + '%', 'Path', info.current, '/', info.total);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const toolpathTime = performance.now() - toolpathStartTime;
|
|
178
|
+
console.log('✓ Toolpaths generated in', toolpathTime.toFixed(1), 'ms');
|
|
179
|
+
|
|
180
|
+
// Analyze results
|
|
181
|
+
console.log('\\nResults:');
|
|
182
|
+
console.log(' Number of output paths:', result.paths.length);
|
|
183
|
+
|
|
184
|
+
const outputPath = result.paths[0];
|
|
185
|
+
const numOutputPoints = outputPath.length / 3;
|
|
186
|
+
console.log(' Output path points:', numOutputPoints);
|
|
187
|
+
console.log(' Input path points:', numSegments);
|
|
188
|
+
console.log(' Densification factor:', (numOutputPoints / numSegments).toFixed(2) + 'x');
|
|
189
|
+
|
|
190
|
+
// Sample Z-values
|
|
191
|
+
const zValues = [];
|
|
192
|
+
for (let i = 0; i < outputPath.length; i += 3) {
|
|
193
|
+
zValues.push(outputPath[i + 2]);
|
|
194
|
+
}
|
|
195
|
+
const minZ = Math.min(...zValues);
|
|
196
|
+
const maxZ = Math.max(...zValues);
|
|
197
|
+
const avgZ = zValues.reduce((a, b) => a + b, 0) / zValues.length;
|
|
198
|
+
|
|
199
|
+
console.log('\\nZ-depth statistics:');
|
|
200
|
+
console.log(' Min Z:', minZ.toFixed(3), 'mm');
|
|
201
|
+
console.log(' Max Z:', maxZ.toFixed(3), 'mm');
|
|
202
|
+
console.log(' Avg Z:', avgZ.toFixed(3), 'mm');
|
|
203
|
+
console.log(' Range:', (maxZ - minZ).toFixed(3), 'mm');
|
|
204
|
+
|
|
205
|
+
// Check GPU-computed maxZ
|
|
206
|
+
console.log('\\nGPU-computed maxZ:');
|
|
207
|
+
console.log(' result.maxZ:', result.maxZ);
|
|
208
|
+
console.log(' result.maxZ[0]:', result.maxZ[0].toFixed(3), 'mm');
|
|
209
|
+
console.log(' CPU maxZ (from walking path):', maxZ.toFixed(3), 'mm');
|
|
210
|
+
console.log(' Match:', Math.abs(result.maxZ[0] - maxZ) < 0.001 ? '✓' : '✗');
|
|
211
|
+
|
|
212
|
+
// Sample and display some output points
|
|
213
|
+
console.log('\\nOutput path samples:');
|
|
214
|
+
const numSamples = Math.min(5, numOutputPoints);
|
|
215
|
+
for (let i = 0; i < numSamples; i++) {
|
|
216
|
+
const idx = Math.floor(i * numOutputPoints / numSamples);
|
|
217
|
+
const x = outputPath[idx * 3].toFixed(2);
|
|
218
|
+
const y = outputPath[idx * 3 + 1].toFixed(2);
|
|
219
|
+
const z = outputPath[idx * 3 + 2].toFixed(2);
|
|
220
|
+
console.log(\` Point \${idx}: (\${x}, \${y}, \${z})\`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Verify that output path lies within terrain XY bounds
|
|
224
|
+
console.log('\\nVerifying path is within terrain bounds...');
|
|
225
|
+
let pathInBounds = true;
|
|
226
|
+
let pointsOutOfBounds = 0;
|
|
227
|
+
for (let i = 0; i < numOutputPoints; i++) {
|
|
228
|
+
const x = outputPath[i * 3];
|
|
229
|
+
const y = outputPath[i * 3 + 1];
|
|
230
|
+
if (x < minX || x > maxX || y < minY || y > maxY) {
|
|
231
|
+
pathInBounds = false;
|
|
232
|
+
pointsOutOfBounds++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (pathInBounds) {
|
|
236
|
+
console.log(' ✓ All path points within terrain bounds');
|
|
237
|
+
} else {
|
|
238
|
+
console.warn(\` ⚠ \${pointsOutOfBounds}/\${numOutputPoints} points outside terrain bounds\`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if all Z values are zFloor (indicates no collision detection)
|
|
242
|
+
const allZFloor = zValues.every(z => z === zFloor);
|
|
243
|
+
if (allZFloor) {
|
|
244
|
+
console.warn(' ⚠ All Z values are zFloor - no terrain collision detected');
|
|
245
|
+
console.warn(' This may indicate a bug in collision detection or path/terrain mismatch');
|
|
246
|
+
} else {
|
|
247
|
+
console.log(' ✓ Terrain collision detected');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Cleanup
|
|
251
|
+
raster.terminate();
|
|
252
|
+
console.log('\\n✓ Test complete');
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
success: true,
|
|
256
|
+
terrainLoad: terrainTime,
|
|
257
|
+
toolLoad: toolTime,
|
|
258
|
+
toolpathGeneration: toolpathTime,
|
|
259
|
+
totalTime: toolTime + terrainTime + toolpathTime,
|
|
260
|
+
inputPoints: numSegments,
|
|
261
|
+
outputPoints: numOutputPoints,
|
|
262
|
+
densificationFactor: numOutputPoints / numSegments,
|
|
263
|
+
zStats: { minZ, maxZ, avgZ, range: maxZ - minZ },
|
|
264
|
+
gpuMaxZ: result.maxZ[0],
|
|
265
|
+
maxZMatch: Math.abs(result.maxZ[0] - maxZ) < 0.001,
|
|
266
|
+
allZFloor: allZFloor
|
|
267
|
+
};
|
|
268
|
+
})();
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
272
|
+
|
|
273
|
+
if (result.error) {
|
|
274
|
+
console.error('❌ Test failed:', result.error);
|
|
275
|
+
app.exit(1);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log('\n=== Test Summary ===');
|
|
280
|
+
console.log('Terrain load:', result.terrainLoad.toFixed(1), 'ms');
|
|
281
|
+
console.log('Tool load:', result.toolLoad.toFixed(1), 'ms');
|
|
282
|
+
console.log('Toolpath generation:', result.toolpathGeneration.toFixed(1), 'ms');
|
|
283
|
+
console.log('Total time:', result.totalTime.toFixed(1), 'ms');
|
|
284
|
+
console.log('\nDensification:', result.inputPoints, '→', result.outputPoints,
|
|
285
|
+
'(' + result.densificationFactor.toFixed(2) + 'x)');
|
|
286
|
+
console.log('Z-depth range:', result.zStats.range.toFixed(3), 'mm');
|
|
287
|
+
console.log('Collision detection:', result.allZFloor ? '❌ FAILED (all zFloor)' : '✓ Working');
|
|
288
|
+
|
|
289
|
+
// Save current results
|
|
290
|
+
fs.writeFileSync(CURRENT_FILE, JSON.stringify(result, null, 2));
|
|
291
|
+
console.log('\n✓ Results saved to:', CURRENT_FILE);
|
|
292
|
+
|
|
293
|
+
console.log('\n✅ Tracing mode test passed');
|
|
294
|
+
app.exit(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
298
|
+
// Forward browser console to Node console (optional, for debugging)
|
|
299
|
+
// console.log('[Browser]', message);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
app.whenReady().then(createWindow);
|
|
304
|
+
|
|
305
|
+
app.on('window-all-closed', () => {
|
|
306
|
+
app.quit();
|
|
307
|
+
});
|
package/src/web/app.js
CHANGED
|
@@ -12,6 +12,7 @@ let zFloor = -100;
|
|
|
12
12
|
let xStep = 5;
|
|
13
13
|
let yStep = 5;
|
|
14
14
|
let angleStep = 1.0; // degrees
|
|
15
|
+
let traceStep = 0.5; // mm - sampling resolution for tracing mode
|
|
15
16
|
let toolSize = 2.5; // mm - target tool diameter
|
|
16
17
|
|
|
17
18
|
let modelSTL = null; // ArrayBuffer (current, possibly rotated)
|
|
@@ -61,6 +62,7 @@ function saveParameters() {
|
|
|
61
62
|
localStorage.setItem('raster-xStep', xStep);
|
|
62
63
|
localStorage.setItem('raster-yStep', yStep);
|
|
63
64
|
localStorage.setItem('raster-angleStep', angleStep);
|
|
65
|
+
localStorage.setItem('raster-traceStep', traceStep);
|
|
64
66
|
localStorage.setItem('raster-toolSize', toolSize);
|
|
65
67
|
console.log(`[App] Saved tool size: ${toolSize}mm`);
|
|
66
68
|
|
|
@@ -127,6 +129,12 @@ function loadParameters() {
|
|
|
127
129
|
document.getElementById('angle-step').value = angleStep;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
const savedTraceStep = localStorage.getItem('raster-traceStep');
|
|
133
|
+
if (savedTraceStep !== null) {
|
|
134
|
+
traceStep = parseFloat(savedTraceStep);
|
|
135
|
+
document.getElementById('trace-step').value = traceStep;
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
const savedToolSize = localStorage.getItem('raster-toolSize');
|
|
131
139
|
if (savedToolSize !== null) {
|
|
132
140
|
toolSize = parseFloat(savedToolSize);
|
|
@@ -545,8 +553,8 @@ async function rasterizeAll() {
|
|
|
545
553
|
updateInfo(`Tool loaded in ${(t1 - t0).toFixed(0)}ms`);
|
|
546
554
|
}
|
|
547
555
|
|
|
548
|
-
if (mode === 'planar') {
|
|
549
|
-
// Planar mode: rasterize terrain immediately
|
|
556
|
+
if (mode === 'planar' || mode === 'tracing') {
|
|
557
|
+
// Planar/Tracing mode: rasterize terrain immediately
|
|
550
558
|
if (modelTriangles) {
|
|
551
559
|
updateInfo('Rasterizing terrain...');
|
|
552
560
|
const t0 = performance.now();
|
|
@@ -557,7 +565,7 @@ async function rasterizeAll() {
|
|
|
557
565
|
const t1 = performance.now();
|
|
558
566
|
updateInfo(`Terrain rasterized in ${(t1 - t0).toFixed(0)}ms`);
|
|
559
567
|
}
|
|
560
|
-
} else {
|
|
568
|
+
} else if (mode === 'radial') {
|
|
561
569
|
// Radial mode: MUST load tool FIRST
|
|
562
570
|
if (!toolTriangles) {
|
|
563
571
|
updateInfo('Error: Radial mode requires tool to be loaded first');
|
|
@@ -608,30 +616,81 @@ async function generateToolpath() {
|
|
|
608
616
|
updateInfo('Model must be rasterized first');
|
|
609
617
|
return;
|
|
610
618
|
}
|
|
611
|
-
} else {
|
|
619
|
+
} else if (mode === 'radial') {
|
|
612
620
|
// Radial mode: terrain must be loaded (stored internally)
|
|
613
621
|
if (!modelTriangles) {
|
|
614
622
|
updateInfo('Model STL must be loaded');
|
|
615
623
|
return;
|
|
616
624
|
}
|
|
625
|
+
} else if (mode === 'tracing') {
|
|
626
|
+
// Tracing mode: terrain must be rasterized
|
|
627
|
+
if (!modelRasterData) {
|
|
628
|
+
updateInfo('Model must be rasterized first');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
617
631
|
}
|
|
618
632
|
|
|
619
633
|
try {
|
|
620
634
|
const t0 = performance.now();
|
|
621
635
|
updateInfo('Generating toolpath...');
|
|
622
636
|
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
637
|
+
// Generate trace paths for tracing mode
|
|
638
|
+
let tracePaths = null;
|
|
639
|
+
if (mode === 'tracing') {
|
|
640
|
+
// Get model bounds from raster data
|
|
641
|
+
const bounds = modelRasterData.bounds;
|
|
642
|
+
const minX = bounds.min.x;
|
|
643
|
+
const maxX = bounds.max.x;
|
|
644
|
+
const minY = bounds.min.y;
|
|
645
|
+
const maxY = bounds.max.y;
|
|
646
|
+
|
|
647
|
+
// Create two cross paths: horizontal through center, vertical through center
|
|
648
|
+
const centerY = (minY + maxY) / 2;
|
|
649
|
+
const centerX = (minX + maxX) / 2;
|
|
650
|
+
|
|
651
|
+
tracePaths = [
|
|
652
|
+
new Float32Array([minX, centerY, maxX, centerY]), // Horizontal line
|
|
653
|
+
new Float32Array([centerX, minY, centerX, maxY]) // Vertical line
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
debug.log(`Generated trace paths: H(${minX.toFixed(2)}, ${centerY.toFixed(2)}) to (${maxX.toFixed(2)}, ${centerY.toFixed(2)})`);
|
|
657
|
+
debug.log(` V(${centerX.toFixed(2)}, ${minY.toFixed(2)}) to (${centerX.toFixed(2)}, ${maxY.toFixed(2)})`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Unified API - works for all modes!
|
|
661
|
+
const generateParams = {
|
|
627
662
|
zFloor: zFloor
|
|
628
|
-
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
if (mode === 'tracing') {
|
|
666
|
+
generateParams.paths = tracePaths;
|
|
667
|
+
generateParams.step = traceStep;
|
|
668
|
+
} else {
|
|
669
|
+
generateParams.xStep = xStep;
|
|
670
|
+
generateParams.yStep = yStep;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
toolpathData = await rasterPath.generateToolpaths(generateParams);
|
|
629
674
|
|
|
630
675
|
const t1 = performance.now();
|
|
631
676
|
|
|
632
677
|
if (mode === 'planar') {
|
|
633
678
|
const numPoints = toolpathData.pathData.length;
|
|
634
679
|
updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
|
|
680
|
+
} else if (mode === 'tracing') {
|
|
681
|
+
const totalPoints = toolpathData.paths.reduce((sum, path) => sum + path.length / 3, 0);
|
|
682
|
+
debug.log(`[Tracing] Generated ${toolpathData.paths.length} paths with ${totalPoints} total points`);
|
|
683
|
+
|
|
684
|
+
// Log sample Z values from each path
|
|
685
|
+
toolpathData.paths.forEach((path, idx) => {
|
|
686
|
+
const zValues = [];
|
|
687
|
+
for (let i = 2; i < Math.min(path.length, 15); i += 3) {
|
|
688
|
+
zValues.push(path[i].toFixed(2));
|
|
689
|
+
}
|
|
690
|
+
debug.log(`[Tracing] Path ${idx} Z samples:`, zValues.join(', '));
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
updateInfo(`Toolpath generated: ${toolpathData.paths.length} paths, ${totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
|
|
635
694
|
} else {
|
|
636
695
|
// debug.log('[Radial] Toolpaths generated:', toolpathData);
|
|
637
696
|
debug.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
|
|
@@ -901,8 +960,8 @@ function displayModelRaster(wrapped) {
|
|
|
901
960
|
const positions = [];
|
|
902
961
|
const colors = [];
|
|
903
962
|
|
|
904
|
-
if (mode === 'planar') {
|
|
905
|
-
// Planar: terrain is dense (Z-only array)
|
|
963
|
+
if (mode === 'planar' || mode === 'tracing') {
|
|
964
|
+
// Planar/Tracing: terrain is dense (Z-only array)
|
|
906
965
|
const { positions: rasterPos, bounds, gridWidth, gridHeight } = modelRasterData;
|
|
907
966
|
const stepSize = resolution;
|
|
908
967
|
|
|
@@ -1057,6 +1116,59 @@ function displayToolpaths(wrapped) {
|
|
|
1057
1116
|
return;
|
|
1058
1117
|
}
|
|
1059
1118
|
|
|
1119
|
+
if (mode === 'tracing') {
|
|
1120
|
+
// Tracing toolpaths - array of XYZ paths
|
|
1121
|
+
const { paths } = toolpathData;
|
|
1122
|
+
|
|
1123
|
+
// Calculate total points
|
|
1124
|
+
let totalPoints = 0;
|
|
1125
|
+
for (const path of paths) {
|
|
1126
|
+
totalPoints += path.length / 3;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
debug.log('[Toolpath Display] Tracing mode:', paths.length, 'paths,', totalPoints, 'total points');
|
|
1130
|
+
|
|
1131
|
+
// Preallocate typed arrays
|
|
1132
|
+
const positions = new Float32Array(totalPoints * 3);
|
|
1133
|
+
const colors = new Float32Array(totalPoints * 3);
|
|
1134
|
+
|
|
1135
|
+
let arrayIdx = 0;
|
|
1136
|
+
for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
|
|
1137
|
+
const path = paths[pathIdx];
|
|
1138
|
+
const numPoints = path.length / 3;
|
|
1139
|
+
|
|
1140
|
+
// Use different colors for each path
|
|
1141
|
+
const color = pathIdx === 0 ? [1, 0.4, 0] : [0, 0.8, 1]; // Orange for horizontal, cyan for vertical
|
|
1142
|
+
|
|
1143
|
+
for (let i = 0; i < numPoints; i++) {
|
|
1144
|
+
positions[arrayIdx] = path[i * 3]; // X
|
|
1145
|
+
positions[arrayIdx + 1] = path[i * 3 + 1]; // Y
|
|
1146
|
+
positions[arrayIdx + 2] = path[i * 3 + 2]; // Z
|
|
1147
|
+
|
|
1148
|
+
colors[arrayIdx] = color[0];
|
|
1149
|
+
colors[arrayIdx + 1] = color[1];
|
|
1150
|
+
colors[arrayIdx + 2] = color[2];
|
|
1151
|
+
|
|
1152
|
+
arrayIdx += 3;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Create geometry
|
|
1157
|
+
const geometry = new THREE.BufferGeometry();
|
|
1158
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1159
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1160
|
+
|
|
1161
|
+
const material = new THREE.PointsMaterial({
|
|
1162
|
+
size: resolution * 1.5,
|
|
1163
|
+
vertexColors: true
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
toolpathPoints = new THREE.Points(geometry, material);
|
|
1167
|
+
rotatedGroup.add(toolpathPoints);
|
|
1168
|
+
|
|
1169
|
+
return; // Exit early for tracing mode
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1060
1172
|
if (mode === 'planar') {
|
|
1061
1173
|
// Planar toolpaths
|
|
1062
1174
|
const { pathData, numScanlines, pointsPerLine } = toolpathData;
|
|
@@ -1324,15 +1436,32 @@ function updateButtonStates() {
|
|
|
1324
1436
|
// ============================================================================
|
|
1325
1437
|
|
|
1326
1438
|
function updateModeUI() {
|
|
1327
|
-
// Show/hide
|
|
1439
|
+
// Show/hide mode-specific controls
|
|
1328
1440
|
const wrappedContainer = document.getElementById('wrapped-container').classList;
|
|
1329
1441
|
const angleStepContainer = document.getElementById('angle-step-container').classList;
|
|
1442
|
+
const traceStepContainer = document.getElementById('trace-step-container').classList;
|
|
1443
|
+
const xStepContainer = document.getElementById('x-step-container').classList;
|
|
1444
|
+
const yStepContainer = document.getElementById('y-step-container').classList;
|
|
1445
|
+
|
|
1330
1446
|
if (mode === 'radial') {
|
|
1331
1447
|
wrappedContainer.remove('hide');
|
|
1332
1448
|
angleStepContainer.remove('hide');
|
|
1449
|
+
traceStepContainer.add('hide');
|
|
1450
|
+
xStepContainer.remove('hide');
|
|
1451
|
+
yStepContainer.remove('hide');
|
|
1452
|
+
} else if (mode === 'tracing') {
|
|
1453
|
+
wrappedContainer.add('hide');
|
|
1454
|
+
angleStepContainer.add('hide');
|
|
1455
|
+
traceStepContainer.remove('hide');
|
|
1456
|
+
xStepContainer.add('hide');
|
|
1457
|
+
yStepContainer.add('hide');
|
|
1333
1458
|
} else {
|
|
1459
|
+
// planar
|
|
1334
1460
|
wrappedContainer.add('hide');
|
|
1335
1461
|
angleStepContainer.add('hide');
|
|
1462
|
+
traceStepContainer.add('hide');
|
|
1463
|
+
xStepContainer.remove('hide');
|
|
1464
|
+
yStepContainer.remove('hide');
|
|
1336
1465
|
}
|
|
1337
1466
|
}
|
|
1338
1467
|
|
|
@@ -1452,6 +1581,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1452
1581
|
updateButtonStates();
|
|
1453
1582
|
});
|
|
1454
1583
|
|
|
1584
|
+
document.getElementById('trace-step').addEventListener('change', (e) => {
|
|
1585
|
+
traceStep = parseFloat(e.target.value);
|
|
1586
|
+
if (mode === 'tracing') {
|
|
1587
|
+
toolpathData = null; // Need to regenerate toolpath
|
|
1588
|
+
}
|
|
1589
|
+
saveParameters();
|
|
1590
|
+
updateInfo(`Trace Step changed to ${traceStep}mm`);
|
|
1591
|
+
updateButtonStates();
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1455
1594
|
// Tool size change
|
|
1456
1595
|
document.getElementById('tool-size').addEventListener('change', async (e) => {
|
|
1457
1596
|
toolSize = parseFloat(e.target.value);
|
package/src/web/index.html
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
<div class="mode-toggle">
|
|
54
54
|
<label><input type="radio" name="mode" value="planar" checked> Planar</label>
|
|
55
55
|
<label><input type="radio" name="mode" value="radial"> Radial</label>
|
|
56
|
+
<label><input type="radio" name="mode" value="tracing"> Tracing</label>
|
|
56
57
|
</div>
|
|
57
58
|
</div>
|
|
58
59
|
|
|
@@ -79,15 +80,18 @@
|
|
|
79
80
|
<label>
|
|
80
81
|
Z Floor: <input type="number" id="z-floor" value="-100" step="10" style="width: 70px;">
|
|
81
82
|
</label>
|
|
82
|
-
<label>
|
|
83
|
+
<label id="x-step-container">
|
|
83
84
|
X Step: <input type="number" id="x-step" value="5" min="1" max="50" style="width: 60px;">
|
|
84
85
|
</label>
|
|
85
|
-
<label>
|
|
86
|
+
<label id="y-step-container">
|
|
86
87
|
Y Step: <input type="number" id="y-step" value="5" min="1" max="50" style="width: 60px;">
|
|
87
88
|
</label>
|
|
88
89
|
<label id="angle-step-container" class="hide">
|
|
89
90
|
Angle Step (deg): <input type="number" id="angle-step" value="1" min="0.1" max="10" step="0.1" style="width: 60px;">
|
|
90
91
|
</label>
|
|
92
|
+
<label id="trace-step-container" class="hide">
|
|
93
|
+
Trace Step (mm): <input type="number" id="trace-step" value="0.5" min="0.1" max="5" step="0.1" style="width: 60px;">
|
|
94
|
+
</label>
|
|
91
95
|
</div>
|
|
92
96
|
|
|
93
97
|
<div class="section">
|