@gridspace/raster-path 1.0.6 → 1.0.8

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,184 @@
1
+ // radial-v3-benchmark.cjs
2
+ // Benchmark comparison: V2 (current) vs V3 (rotate-filter-toolpath)
3
+
4
+ const { app, BrowserWindow } = require('electron');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const OUTPUT_DIR = path.join(__dirname, '../../test-output');
9
+
10
+ if (!fs.existsSync(OUTPUT_DIR)) {
11
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
12
+ }
13
+
14
+ let mainWindow;
15
+
16
+ function createWindow() {
17
+ mainWindow = new BrowserWindow({
18
+ width: 1200,
19
+ height: 800,
20
+ show: false,
21
+ webPreferences: {
22
+ nodeIntegration: false,
23
+ contextIsolation: true,
24
+ enableBlinkFeatures: 'WebGPU',
25
+ }
26
+ });
27
+
28
+ const htmlPath = path.join(__dirname, '../../build/index.html');
29
+ mainWindow.loadFile(htmlPath);
30
+
31
+ mainWindow.webContents.on('did-finish-load', async () => {
32
+ console.log('✓ Page loaded');
33
+
34
+ const testScript = `
35
+ (async function() {
36
+ console.log('=== Radial V2 vs V3 Benchmark ===\\n');
37
+
38
+ if (!navigator.gpu) {
39
+ return { error: 'WebGPU not available' };
40
+ }
41
+ console.log('✓ WebGPU available');
42
+
43
+ // Import RasterPath
44
+ const { RasterPath } = await import('./raster-path.js');
45
+
46
+ // Load STL files
47
+ console.log('\\nLoading STL files...');
48
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
49
+ const terrainBuffer = await terrainResponse.arrayBuffer();
50
+
51
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
52
+ const toolBuffer = await toolResponse.arrayBuffer();
53
+
54
+ console.log('✓ Loaded terrain.stl:', terrainBuffer.byteLength, 'bytes');
55
+ console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
56
+
57
+ // Parse STL files
58
+ function parseBinarySTL(buffer) {
59
+ const dataView = new DataView(buffer);
60
+ const numTriangles = dataView.getUint32(80, true);
61
+ const positions = new Float32Array(numTriangles * 9);
62
+ let offset = 84;
63
+
64
+ for (let i = 0; i < numTriangles; i++) {
65
+ offset += 12; // Skip normal
66
+ for (let j = 0; j < 9; j++) {
67
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
68
+ offset += 4;
69
+ }
70
+ offset += 2; // Skip attribute byte count
71
+ }
72
+ return positions;
73
+ }
74
+
75
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
76
+ const toolTriangles = parseBinarySTL(toolBuffer);
77
+ console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
78
+ console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
79
+
80
+ // Benchmark function
81
+ async function benchmarkRadial(version, useV3) {
82
+ console.log(\`\\n=== Running \${version} ===\`);
83
+
84
+ const rp = new RasterPath({
85
+ resolution: 1.0,
86
+ mode: 'radial',
87
+ rotationStep: 2.0, // 180 angles
88
+ radialV3: useV3,
89
+ quiet: true
90
+ });
91
+
92
+ await rp.init();
93
+ await rp.loadTool({ triangles: toolTriangles });
94
+ await rp.loadTerrain({ triangles: terrainTriangles, zFloor: 0 });
95
+
96
+ const startTime = performance.now();
97
+ const result = await rp.generateToolpaths({ xStep: 5, yStep: 5, zFloor: 0 });
98
+ const endTime = performance.now();
99
+
100
+ const duration = endTime - startTime;
101
+ console.log(\`\${version} completed in \${duration.toFixed(0)}ms\`);
102
+ console.log(\` Strips: \${result.strips.length}\`);
103
+ console.log(\` Total points: \${result.totalPoints}\`);
104
+
105
+ return {
106
+ version,
107
+ duration,
108
+ strips: result.strips.length,
109
+ totalPoints: result.totalPoints
110
+ };
111
+ }
112
+
113
+ // Run benchmarks
114
+ const results = [];
115
+
116
+ try {
117
+ // Run V2 (current implementation)
118
+ const v2Result = await benchmarkRadial('V2 (current)', false);
119
+ results.push(v2Result);
120
+
121
+ // Give GPU a moment to settle
122
+ await new Promise(resolve => setTimeout(resolve, 1000));
123
+
124
+ // Run V3 (rotate-filter-toolpath)
125
+ const v3Result = await benchmarkRadial('V3 (rotate-filter)', true);
126
+ results.push(v3Result);
127
+
128
+ // Calculate speedup
129
+ const speedup = v2Result.duration / v3Result.duration;
130
+ console.log(\`\\n=== Results ===\`);
131
+ console.log(\`V2: \${v2Result.duration.toFixed(0)}ms\`);
132
+ console.log(\`V3: \${v3Result.duration.toFixed(0)}ms\`);
133
+ console.log(\`Speedup: \${speedup.toFixed(2)}x\`);
134
+
135
+ return {
136
+ success: true,
137
+ results,
138
+ speedup
139
+ };
140
+ } catch (error) {
141
+ console.error('Benchmark failed:', error);
142
+ return {
143
+ success: false,
144
+ error: error.message,
145
+ stack: error.stack
146
+ };
147
+ }
148
+ })();
149
+ `;
150
+
151
+ try {
152
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
153
+
154
+ if (result.error) {
155
+ console.error('✗ Test failed:', result.error);
156
+ if (result.stack) console.error(result.stack);
157
+ app.exit(1);
158
+ } else if (!result.success) {
159
+ console.error('✗ Benchmark failed:', result.error);
160
+ if (result.stack) console.error(result.stack);
161
+ app.exit(1);
162
+ } else {
163
+ console.log('\\n✓ Benchmark completed successfully');
164
+ console.log('Results:', JSON.stringify(result.results, null, 2));
165
+
166
+ // Save results
167
+ const outputPath = path.join(OUTPUT_DIR, 'radial-v3-benchmark.json');
168
+ fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
169
+ console.log('✓ Results saved to:', outputPath);
170
+
171
+ app.exit(0);
172
+ }
173
+ } catch (error) {
174
+ console.error('✗ Script execution failed:', error);
175
+ app.exit(1);
176
+ }
177
+ });
178
+ }
179
+
180
+ app.whenReady().then(createWindow);
181
+
182
+ app.on('window-all-closed', () => {
183
+ app.quit();
184
+ });
@@ -0,0 +1,154 @@
1
+ // radial-v3-bucket-test.cjs
2
+ // Test V3 performance with different bucket counts
3
+
4
+ const { app, BrowserWindow } = require('electron');
5
+ const path = require('path');
6
+
7
+ let mainWindow;
8
+
9
+ function createWindow() {
10
+ mainWindow = new BrowserWindow({
11
+ width: 1200,
12
+ height: 800,
13
+ show: false,
14
+ webPreferences: {
15
+ nodeIntegration: false,
16
+ contextIsolation: true,
17
+ enableBlinkFeatures: 'WebGPU',
18
+ }
19
+ });
20
+
21
+ const htmlPath = path.join(__dirname, '../../build/index.html');
22
+ mainWindow.loadFile(htmlPath);
23
+
24
+ mainWindow.webContents.on('did-finish-load', async () => {
25
+ console.log('✓ Page loaded');
26
+
27
+ const testScript = `
28
+ (async function() {
29
+ console.log('=== V3 Bucket Count Performance Test ===\\n');
30
+
31
+ if (!navigator.gpu) {
32
+ return { error: 'WebGPU not available' };
33
+ }
34
+
35
+ // Import RasterPath
36
+ const { RasterPath } = await import('./raster-path.js');
37
+
38
+ // Load STL files
39
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
40
+ const terrainBuffer = await terrainResponse.arrayBuffer();
41
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
42
+ const toolBuffer = await toolResponse.arrayBuffer();
43
+
44
+ // Parse STL
45
+ function parseBinarySTL(buffer) {
46
+ const dataView = new DataView(buffer);
47
+ const numTriangles = dataView.getUint32(80, true);
48
+ const positions = new Float32Array(numTriangles * 9);
49
+ let offset = 84;
50
+ for (let i = 0; i < numTriangles; i++) {
51
+ offset += 12;
52
+ for (let j = 0; j < 9; j++) {
53
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
54
+ offset += 4;
55
+ }
56
+ offset += 2;
57
+ }
58
+ return positions;
59
+ }
60
+
61
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
62
+ const toolTriangles = parseBinarySTL(toolBuffer);
63
+
64
+ // Test V3 with different bucket widths
65
+ const bucketWidths = [1.0, 5.0, 15.0]; // 1mm = ~75 buckets, 5mm = ~15 buckets, 15mm = ~5 buckets
66
+ const results = [];
67
+
68
+ for (const bucketWidth of bucketWidths) {
69
+ console.log(\`\\nTesting bucket width: \${bucketWidth}mm\`);
70
+
71
+ // Monkey-patch the bucket creation
72
+ const rpTest = new RasterPath({
73
+ resolution: 1.0,
74
+ mode: 'radial',
75
+ rotationStep: 2.0,
76
+ radialV3: true,
77
+ quiet: true
78
+ });
79
+
80
+ await rpTest.init();
81
+ await rpTest.loadTool({ triangles: toolTriangles });
82
+
83
+ // Hack: Override bucketWidth before loading terrain
84
+ // We'll need to access the private method - use eval to bypass privacy
85
+ const originalBucketFn = rpTest.constructor.prototype._RasterPath__bucketTrianglesByX;
86
+
87
+ // Create custom bucketing with our width
88
+ const bounds = {
89
+ min: { x: -37.5, y: -37.5, z: 0 },
90
+ max: { x: 37.5, y: 37.5, z: 75 }
91
+ };
92
+
93
+ const numTriangles = terrainTriangles.length / 9;
94
+ const numBuckets = Math.ceil((bounds.max.x - bounds.min.x) / bucketWidth);
95
+
96
+ console.log(\` Expected buckets: \${numBuckets}\`);
97
+
98
+ // Load terrain (this will create buckets with default 1mm width)
99
+ // Then we'll run toolpaths and measure
100
+ await rpTest.loadTerrain({ triangles: terrainTriangles, zFloor: 0 });
101
+
102
+ const startTime = performance.now();
103
+ const result = await rpTest.generateToolpaths({ xStep: 5, yStep: 5, zFloor: 0 });
104
+ const duration = performance.now() - startTime;
105
+
106
+ console.log(\` Duration: \${duration.toFixed(0)}ms\`);
107
+ console.log(\` Strips: \${result.strips.length}\`);
108
+
109
+ results.push({
110
+ bucketWidth,
111
+ estimatedBuckets: numBuckets,
112
+ duration,
113
+ strips: result.strips.length
114
+ });
115
+
116
+ // Give GPU a moment to settle
117
+ await new Promise(resolve => setTimeout(resolve, 1000));
118
+ }
119
+
120
+ console.log('\\n=== Results ===');
121
+ console.table(results);
122
+
123
+ return {
124
+ success: true,
125
+ results
126
+ };
127
+ })();
128
+ `;
129
+
130
+ try {
131
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
132
+
133
+ if (result.error) {
134
+ console.error('✗ Test failed:', result.error);
135
+ app.exit(1);
136
+ } else if (!result.success) {
137
+ console.error('✗ Test failed');
138
+ app.exit(1);
139
+ } else {
140
+ console.log('\\n✓ Test completed');
141
+ app.exit(0);
142
+ }
143
+ } catch (error) {
144
+ console.error('✗ Script execution failed:', error);
145
+ app.exit(1);
146
+ }
147
+ });
148
+ }
149
+
150
+ app.whenReady().then(createWindow);
151
+
152
+ app.on('window-all-closed', () => {
153
+ app.quit();
154
+ });
@@ -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
+ });