@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.
@@ -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
- // Unified API - works for both modes!
624
- toolpathData = await rasterPath.generateToolpaths({
625
- xStep: xStep,
626
- yStep: yStep,
637
+ // Generate trace paths for tracing mode
638
+ let tracePaths = null;
639
+ if (mode === 'tracing') {
640
+ // Get model bounds from raster data
641
+ const bounds = modelRasterData.bounds;
642
+ const minX = bounds.min.x;
643
+ const maxX = bounds.max.x;
644
+ const minY = bounds.min.y;
645
+ const maxY = bounds.max.y;
646
+
647
+ // Create two cross paths: horizontal through center, vertical through center
648
+ const centerY = (minY + maxY) / 2;
649
+ const centerX = (minX + maxX) / 2;
650
+
651
+ tracePaths = [
652
+ new Float32Array([minX, centerY, maxX, centerY]), // Horizontal line
653
+ new Float32Array([centerX, minY, centerX, maxY]) // Vertical line
654
+ ];
655
+
656
+ debug.log(`Generated trace paths: H(${minX.toFixed(2)}, ${centerY.toFixed(2)}) to (${maxX.toFixed(2)}, ${centerY.toFixed(2)})`);
657
+ debug.log(` V(${centerX.toFixed(2)}, ${minY.toFixed(2)}) to (${centerX.toFixed(2)}, ${maxY.toFixed(2)})`);
658
+ }
659
+
660
+ // Unified API - works for all modes!
661
+ const generateParams = {
627
662
  zFloor: zFloor
628
- });
663
+ };
664
+
665
+ if (mode === 'tracing') {
666
+ generateParams.paths = tracePaths;
667
+ generateParams.step = traceStep;
668
+ } else {
669
+ generateParams.xStep = xStep;
670
+ generateParams.yStep = yStep;
671
+ }
672
+
673
+ toolpathData = await rasterPath.generateToolpaths(generateParams);
629
674
 
630
675
  const t1 = performance.now();
631
676
 
632
677
  if (mode === 'planar') {
633
678
  const numPoints = toolpathData.pathData.length;
634
679
  updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
680
+ } else if (mode === 'tracing') {
681
+ const totalPoints = toolpathData.paths.reduce((sum, path) => sum + path.length / 3, 0);
682
+ debug.log(`[Tracing] Generated ${toolpathData.paths.length} paths with ${totalPoints} total points`);
683
+
684
+ // Log sample Z values from each path
685
+ toolpathData.paths.forEach((path, idx) => {
686
+ const zValues = [];
687
+ for (let i = 2; i < Math.min(path.length, 15); i += 3) {
688
+ zValues.push(path[i].toFixed(2));
689
+ }
690
+ debug.log(`[Tracing] Path ${idx} Z samples:`, zValues.join(', '));
691
+ });
692
+
693
+ updateInfo(`Toolpath generated: ${toolpathData.paths.length} paths, ${totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
635
694
  } else {
636
695
  // debug.log('[Radial] Toolpaths generated:', toolpathData);
637
696
  debug.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
@@ -901,8 +960,8 @@ function displayModelRaster(wrapped) {
901
960
  const positions = [];
902
961
  const colors = [];
903
962
 
904
- if (mode === 'planar') {
905
- // Planar: terrain is dense (Z-only array)
963
+ if (mode === 'planar' || mode === 'tracing') {
964
+ // Planar/Tracing: terrain is dense (Z-only array)
906
965
  const { positions: rasterPos, bounds, gridWidth, gridHeight } = modelRasterData;
907
966
  const stepSize = resolution;
908
967
 
@@ -1057,6 +1116,59 @@ function displayToolpaths(wrapped) {
1057
1116
  return;
1058
1117
  }
1059
1118
 
1119
+ if (mode === 'tracing') {
1120
+ // Tracing toolpaths - array of XYZ paths
1121
+ const { paths } = toolpathData;
1122
+
1123
+ // Calculate total points
1124
+ let totalPoints = 0;
1125
+ for (const path of paths) {
1126
+ totalPoints += path.length / 3;
1127
+ }
1128
+
1129
+ debug.log('[Toolpath Display] Tracing mode:', paths.length, 'paths,', totalPoints, 'total points');
1130
+
1131
+ // Preallocate typed arrays
1132
+ const positions = new Float32Array(totalPoints * 3);
1133
+ const colors = new Float32Array(totalPoints * 3);
1134
+
1135
+ let arrayIdx = 0;
1136
+ for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
1137
+ const path = paths[pathIdx];
1138
+ const numPoints = path.length / 3;
1139
+
1140
+ // Use different colors for each path
1141
+ const color = pathIdx === 0 ? [1, 0.4, 0] : [0, 0.8, 1]; // Orange for horizontal, cyan for vertical
1142
+
1143
+ for (let i = 0; i < numPoints; i++) {
1144
+ positions[arrayIdx] = path[i * 3]; // X
1145
+ positions[arrayIdx + 1] = path[i * 3 + 1]; // Y
1146
+ positions[arrayIdx + 2] = path[i * 3 + 2]; // Z
1147
+
1148
+ colors[arrayIdx] = color[0];
1149
+ colors[arrayIdx + 1] = color[1];
1150
+ colors[arrayIdx + 2] = color[2];
1151
+
1152
+ arrayIdx += 3;
1153
+ }
1154
+ }
1155
+
1156
+ // Create geometry
1157
+ const geometry = new THREE.BufferGeometry();
1158
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1159
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
1160
+
1161
+ const material = new THREE.PointsMaterial({
1162
+ size: resolution * 1.5,
1163
+ vertexColors: true
1164
+ });
1165
+
1166
+ toolpathPoints = new THREE.Points(geometry, material);
1167
+ rotatedGroup.add(toolpathPoints);
1168
+
1169
+ return; // Exit early for tracing mode
1170
+ }
1171
+
1060
1172
  if (mode === 'planar') {
1061
1173
  // Planar toolpaths
1062
1174
  const { pathData, numScanlines, pointsPerLine } = toolpathData;
@@ -1324,15 +1436,32 @@ function updateButtonStates() {
1324
1436
  // ============================================================================
1325
1437
 
1326
1438
  function updateModeUI() {
1327
- // Show/hide wrapped toggle and angle step for radial mode
1439
+ // Show/hide mode-specific controls
1328
1440
  const wrappedContainer = document.getElementById('wrapped-container').classList;
1329
1441
  const angleStepContainer = document.getElementById('angle-step-container').classList;
1442
+ const traceStepContainer = document.getElementById('trace-step-container').classList;
1443
+ const xStepContainer = document.getElementById('x-step-container').classList;
1444
+ const yStepContainer = document.getElementById('y-step-container').classList;
1445
+
1330
1446
  if (mode === 'radial') {
1331
1447
  wrappedContainer.remove('hide');
1332
1448
  angleStepContainer.remove('hide');
1449
+ traceStepContainer.add('hide');
1450
+ xStepContainer.remove('hide');
1451
+ yStepContainer.remove('hide');
1452
+ } else if (mode === 'tracing') {
1453
+ wrappedContainer.add('hide');
1454
+ angleStepContainer.add('hide');
1455
+ traceStepContainer.remove('hide');
1456
+ xStepContainer.add('hide');
1457
+ yStepContainer.add('hide');
1333
1458
  } else {
1459
+ // planar
1334
1460
  wrappedContainer.add('hide');
1335
1461
  angleStepContainer.add('hide');
1462
+ traceStepContainer.add('hide');
1463
+ xStepContainer.remove('hide');
1464
+ yStepContainer.remove('hide');
1336
1465
  }
1337
1466
  }
1338
1467
 
@@ -1452,6 +1581,16 @@ document.addEventListener('DOMContentLoaded', async () => {
1452
1581
  updateButtonStates();
1453
1582
  });
1454
1583
 
1584
+ document.getElementById('trace-step').addEventListener('change', (e) => {
1585
+ traceStep = parseFloat(e.target.value);
1586
+ if (mode === 'tracing') {
1587
+ toolpathData = null; // Need to regenerate toolpath
1588
+ }
1589
+ saveParameters();
1590
+ updateInfo(`Trace Step changed to ${traceStep}mm`);
1591
+ updateButtonStates();
1592
+ });
1593
+
1455
1594
  // Tool size change
1456
1595
  document.getElementById('tool-size').addEventListener('change', async (e) => {
1457
1596
  toolSize = parseFloat(e.target.value);
@@ -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">