@gridspace/raster-path 1.0.3 → 1.0.5

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,167 @@
1
+ // extreme-work-test.cjs
2
+ // Test if there's ANY per-thread compute limit by pushing to extreme levels
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('=== Extreme Per-Thread Work Test ===');
30
+ console.log('Testing if there is ANY limit to work per thread...\\n');
31
+
32
+ if (!navigator.gpu) {
33
+ return { error: 'WebGPU not available' };
34
+ }
35
+
36
+ const { RasterPath } = await import('./raster-path.js');
37
+
38
+ const raster = new RasterPath({ mode: 'planar', resolution: 0.1 });
39
+ await raster.init();
40
+
41
+ // Test progressively larger workloads on a SINGLE 16x16x1 workgroup (256 threads)
42
+ const testLevels = [
43
+ { tests: 1_000_000_000, label: '1 billion' },
44
+ { tests: 2_000_000_000, label: '2 billion' },
45
+ { tests: 5_000_000_000, label: '5 billion' },
46
+ { tests: 10_000_000_000, label: '10 billion' },
47
+ ];
48
+
49
+ const results = [];
50
+
51
+ for (const level of testLevels) {
52
+ console.log(\`Testing \${level.label} tests/thread (\${level.tests.toLocaleString()})...\`);
53
+
54
+ const calibrationPromise = new Promise((resolve, reject) => {
55
+ const timeout = setTimeout(() => {
56
+ reject(new Error('Test timed out after 30s'));
57
+ }, 30000);
58
+
59
+ const handler = raster.worker.onmessage;
60
+ raster.worker.onmessage = (e) => {
61
+ if (e.data.type === 'calibrate-complete') {
62
+ clearTimeout(timeout);
63
+ resolve(e.data.data);
64
+ } else if (e.data.type === 'error') {
65
+ clearTimeout(timeout);
66
+ reject(new Error(e.data.message));
67
+ } else {
68
+ handler(e);
69
+ }
70
+ };
71
+ });
72
+
73
+ const startTime = performance.now();
74
+
75
+ raster.worker.postMessage({
76
+ type: 'calibrate',
77
+ data: {
78
+ calibrationType: 'workgroup',
79
+ options: {
80
+ workgroupSizes: [[16, 16, 1]],
81
+ minWork: level.tests,
82
+ maxWork: level.tests,
83
+ verbose: false,
84
+ }
85
+ }
86
+ });
87
+
88
+ try {
89
+ const result = await calibrationPromise;
90
+ const elapsed = performance.now() - startTime;
91
+ const success = result.safeWorkloadMatrix[0]?.maxWork === level.tests;
92
+
93
+ const status = success ? '✓' : '❌';
94
+ const totalTests = (level.tests * 256).toLocaleString();
95
+ console.log(\` \${status} \${level.label}: \${elapsed.toFixed(0)}ms (\${totalTests} total tests)\\n\`);
96
+
97
+ results.push({
98
+ tests: level.tests,
99
+ label: level.label,
100
+ success,
101
+ elapsed,
102
+ });
103
+
104
+ if (!success) {
105
+ console.log('Found failure point - stopping test.');
106
+ break;
107
+ }
108
+ } catch (error) {
109
+ console.log(\` ❌ \${level.label}: FAILED - \${error.message}\\n\`);
110
+ results.push({
111
+ tests: level.tests,
112
+ label: level.label,
113
+ success: false,
114
+ error: error.message,
115
+ });
116
+ break;
117
+ }
118
+ }
119
+
120
+ console.log('\\n=== Summary ===');
121
+ for (const r of results) {
122
+ const status = r.success ? '✓' : '❌';
123
+ const time = r.elapsed ? \` in \${r.elapsed.toFixed(0)}ms\` : '';
124
+ const err = r.error ? \` - \${r.error}\` : '';
125
+ console.log(\`\${status} \${r.label}\${time}\${err}\`);
126
+ }
127
+
128
+ const maxSuccess = results.filter(r => r.success).pop();
129
+ if (maxSuccess) {
130
+ console.log(\`\\nMax verified work per thread: \${maxSuccess.label} (\${maxSuccess.tests.toLocaleString()} tests)\`);
131
+ console.log('Conclusion: No practical per-thread compute limit detected');
132
+ } else {
133
+ console.log('\\nFound per-thread compute limit');
134
+ }
135
+
136
+ return { success: true, results };
137
+ })();
138
+ `;
139
+
140
+ try {
141
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
142
+
143
+ if (result.error) {
144
+ console.error('❌ Test failed:', result.error);
145
+ app.exit(1);
146
+ } else {
147
+ console.log('\n✅ Extreme work test complete');
148
+ app.exit(0);
149
+ }
150
+ } catch (error) {
151
+ console.error('❌ Test error:', error);
152
+ app.exit(1);
153
+ }
154
+ });
155
+
156
+ mainWindow.webContents.on('console-message', (event, level, message) => {
157
+ console.log(message);
158
+ });
159
+ }
160
+
161
+ app.whenReady().then(createWindow);
162
+
163
+ app.on('window-all-closed', () => {
164
+ if (process.platform !== 'darwin') {
165
+ app.quit();
166
+ }
167
+ });
@@ -0,0 +1,334 @@
1
+ // lathe-cylinder-2-debug.cjs
2
+ // Debug test for lathe-cylinder-2.stl with specific parameters
3
+ // Run multiple times to check for non-deterministic behavior
4
+
5
+ const { app, BrowserWindow } = require('electron');
6
+ const path = require('path');
7
+
8
+ const TEST_ITERATIONS = 5; // Run test 5 times to check consistency
9
+
10
+ const TEST_CONFIG = {
11
+ model: '../benchmark/fixtures/lathe-cylinder-2.stl',
12
+ tool: '../benchmark/fixtures/tool.stl',
13
+ resolution: 0.05,
14
+ rotationStep: 1.0,
15
+ toolDiameter: 5.0, // Scale tool to 5mm
16
+ xStep: 1,
17
+ yStep: 1
18
+ };
19
+
20
+ console.log('=== Lathe Cylinder 2 Debug Test ===');
21
+ console.log('Testing for non-deterministic behavior / missing toolpaths');
22
+ console.log(`Model: lathe-cylinder-2.stl`);
23
+ console.log(`Resolution: ${TEST_CONFIG.resolution}mm`);
24
+ console.log(`Tool size: ${TEST_CONFIG.toolDiameter}mm`);
25
+ console.log(`Rotation step: ${TEST_CONFIG.rotationStep}°`);
26
+ console.log(`XY steps: ${TEST_CONFIG.xStep}`);
27
+ console.log(`Iterations: ${TEST_ITERATIONS}`);
28
+ console.log('');
29
+
30
+ let win;
31
+ let testResults = [];
32
+ let currentIteration = 0;
33
+
34
+ app.whenReady().then(async () => {
35
+ win = new BrowserWindow({
36
+ width: 1200,
37
+ height: 800,
38
+ show: false,
39
+ webPreferences: {
40
+ nodeIntegration: false,
41
+ contextIsolation: true
42
+ }
43
+ });
44
+
45
+ // Capture console messages
46
+ win.webContents.on('console-message', (event, level, message) => {
47
+ console.log(message);
48
+ });
49
+
50
+ await win.loadFile(path.join(__dirname, '../../build/index.html'));
51
+ console.log('✓ Page loaded\n');
52
+
53
+ // Run test multiple times
54
+ await runNextIteration();
55
+ });
56
+
57
+ async function runNextIteration() {
58
+ if (currentIteration >= TEST_ITERATIONS) {
59
+ // All iterations complete - analyze results
60
+ analyzeResults();
61
+ app.quit();
62
+ return;
63
+ }
64
+
65
+ currentIteration++;
66
+ console.log('======================================================================');
67
+ console.log(`ITERATION ${currentIteration} of ${TEST_ITERATIONS}`);
68
+ console.log('======================================================================');
69
+
70
+ try {
71
+ const result = await runTest();
72
+ testResults.push(result);
73
+
74
+ // Wait a bit between iterations
75
+ await new Promise(resolve => setTimeout(resolve, 1000));
76
+
77
+ // Run next iteration
78
+ await runNextIteration();
79
+ } catch (error) {
80
+ console.error(`✗ Iteration ${currentIteration} failed:`, error);
81
+ testResults.push({
82
+ iteration: currentIteration,
83
+ success: false,
84
+ error: error.message
85
+ });
86
+
87
+ // Continue with next iteration even on failure
88
+ await new Promise(resolve => setTimeout(resolve, 1000));
89
+ await runNextIteration();
90
+ }
91
+ }
92
+
93
+ async function runTest() {
94
+ const result = await win.webContents.executeJavaScript(`
95
+ (async () => {
96
+ const startTime = performance.now();
97
+
98
+ // Load model
99
+ const modelResponse = await fetch('${TEST_CONFIG.model}');
100
+ const modelBuffer = await modelResponse.arrayBuffer();
101
+
102
+ // Parse model triangles
103
+ function parseSTL(arrayBuffer) {
104
+ const view = new DataView(arrayBuffer);
105
+ const numTriangles = view.getUint32(80, true);
106
+ const triangles = new Float32Array(numTriangles * 9);
107
+
108
+ let offset = 84;
109
+ for (let i = 0; i < numTriangles; i++) {
110
+ offset += 12; // Skip normal
111
+ for (let v = 0; v < 3; v++) {
112
+ triangles[i * 9 + v * 3 + 0] = view.getFloat32(offset + 0, true);
113
+ triangles[i * 9 + v * 3 + 1] = view.getFloat32(offset + 4, true);
114
+ triangles[i * 9 + v * 3 + 2] = view.getFloat32(offset + 8, true);
115
+ offset += 12;
116
+ }
117
+ offset += 2; // Skip attribute
118
+ }
119
+
120
+ return triangles;
121
+ }
122
+
123
+ const modelTriangles = parseSTL(modelBuffer);
124
+
125
+ // Load and scale tool
126
+ const toolResponse = await fetch('${TEST_CONFIG.tool}');
127
+ const toolBuffer = await toolResponse.arrayBuffer();
128
+ const toolTriangles = parseSTL(toolBuffer);
129
+
130
+ // Scale tool to target diameter (${TEST_CONFIG.toolDiameter}mm)
131
+ function calculateToolDiameter(triangles) {
132
+ let minX = Infinity, maxX = -Infinity;
133
+ let minY = Infinity, maxY = -Infinity;
134
+ for (let i = 0; i < triangles.length; i += 3) {
135
+ minX = Math.min(minX, triangles[i]);
136
+ maxX = Math.max(maxX, triangles[i]);
137
+ minY = Math.min(minY, triangles[i + 1]);
138
+ maxY = Math.max(maxY, triangles[i + 1]);
139
+ }
140
+ return Math.max(maxX - minX, maxY - minY);
141
+ }
142
+
143
+ const originalDiameter = calculateToolDiameter(toolTriangles);
144
+ const scale = ${TEST_CONFIG.toolDiameter} / originalDiameter;
145
+ const scaledToolTriangles = new Float32Array(toolTriangles.length);
146
+ for (let i = 0; i < toolTriangles.length; i++) {
147
+ scaledToolTriangles[i] = toolTriangles[i] * scale;
148
+ }
149
+
150
+ // Import worker
151
+ const { RasterPath } = await import('./raster-path.js');
152
+ const rasterPath = new RasterPath({
153
+ mode: 'radial',
154
+ resolution: ${TEST_CONFIG.resolution},
155
+ rotationStep: ${TEST_CONFIG.rotationStep}
156
+ });
157
+
158
+ // Initialize
159
+ await rasterPath.init();
160
+
161
+ const toolStart = performance.now();
162
+ await rasterPath.loadTool({ triangles: scaledToolTriangles });
163
+ const toolTime = performance.now() - toolStart;
164
+
165
+ const terrainStart = performance.now();
166
+ await rasterPath.loadTerrain({
167
+ triangles: modelTriangles,
168
+ zFloor: 0
169
+ });
170
+ const terrainTime = performance.now() - terrainStart;
171
+
172
+ const toolpathStart = performance.now();
173
+ const toolpathResult = await rasterPath.generateToolpaths({
174
+ xStep: ${TEST_CONFIG.xStep},
175
+ yStep: ${TEST_CONFIG.yStep},
176
+ zFloor: 0,
177
+ radiusOffset: 20
178
+ });
179
+ const toolpathTime = performance.now() - toolpathStart;
180
+
181
+ const totalTime = performance.now() - startTime;
182
+
183
+ // Count strips and points
184
+ let numStrips = 0;
185
+ let totalPoints = 0;
186
+ let minPoints = Infinity;
187
+ let maxPoints = -Infinity;
188
+ let emptyStrips = 0;
189
+
190
+ if (toolpathResult) {
191
+ numStrips = toolpathResult.numStrips || 0;
192
+ totalPoints = toolpathResult.totalPoints || 0;
193
+
194
+ // If we have the raw strips data, analyze it
195
+ if (toolpathResult.strips) {
196
+ for (const strip of toolpathResult.strips) {
197
+ const pointCount = strip.length / 3;
198
+ if (pointCount === 0) {
199
+ emptyStrips++;
200
+ }
201
+ minPoints = Math.min(minPoints, pointCount);
202
+ maxPoints = Math.max(maxPoints, pointCount);
203
+ }
204
+ } else if (toolpathResult.result && toolpathResult.result.strips) {
205
+ // Alternative format
206
+ numStrips = toolpathResult.result.strips.length;
207
+ for (const strip of toolpathResult.result.strips) {
208
+ const pointCount = strip.length / 3;
209
+ totalPoints += pointCount;
210
+ if (pointCount === 0) {
211
+ emptyStrips++;
212
+ }
213
+ minPoints = Math.min(minPoints, pointCount);
214
+ maxPoints = Math.max(maxPoints, pointCount);
215
+ }
216
+ }
217
+ }
218
+
219
+ return {
220
+ success: true,
221
+ triangleCount: modelTriangles.length / 9,
222
+ timing: {
223
+ terrain: terrainTime,
224
+ tool: toolTime,
225
+ toolpath: toolpathTime,
226
+ total: totalTime
227
+ },
228
+ result: {
229
+ numStrips,
230
+ totalPoints,
231
+ minPoints: minPoints === Infinity ? 0 : minPoints,
232
+ maxPoints: maxPoints === -Infinity ? 0 : maxPoints,
233
+ emptyStrips,
234
+ avgPointsPerStrip: numStrips > 0 ? (totalPoints / numStrips).toFixed(1) : 0
235
+ }
236
+ };
237
+ })()
238
+ `);
239
+
240
+ console.log(`\nResults (Iteration ${currentIteration}):`);
241
+ console.log(` Triangle count: ${result.triangleCount.toLocaleString()}`);
242
+ console.log(` Terrain time: ${result.timing.terrain.toFixed(1)}ms`);
243
+ console.log(` Tool time: ${result.timing.tool.toFixed(1)}ms`);
244
+ console.log(` Toolpath time: ${result.timing.toolpath.toFixed(1)}ms`);
245
+ console.log(` Total time: ${result.timing.total.toFixed(1)}ms`);
246
+ console.log('');
247
+ console.log('Toolpath Output:');
248
+ console.log(` Strips: ${result.result.numStrips}`);
249
+ console.log(` Total points: ${result.result.totalPoints.toLocaleString()}`);
250
+ console.log(` Empty strips: ${result.result.emptyStrips}`);
251
+ console.log(` Min points/strip: ${result.result.minPoints}`);
252
+ console.log(` Max points/strip: ${result.result.maxPoints}`);
253
+ console.log(` Avg points/strip: ${result.result.avgPointsPerStrip}`);
254
+ console.log('');
255
+
256
+ return {
257
+ iteration: currentIteration,
258
+ ...result
259
+ };
260
+ }
261
+
262
+ function analyzeResults() {
263
+ console.log('');
264
+ console.log('======================================================================');
265
+ console.log('CONSISTENCY ANALYSIS');
266
+ console.log('======================================================================');
267
+
268
+ const successful = testResults.filter(r => r.success);
269
+
270
+ if (successful.length === 0) {
271
+ console.log('✗ All iterations failed!');
272
+ return;
273
+ }
274
+
275
+ if (successful.length < TEST_ITERATIONS) {
276
+ console.log(`⚠️ Only ${successful.length} of ${TEST_ITERATIONS} iterations succeeded`);
277
+ }
278
+
279
+ // Check consistency of results
280
+ const totalPointsCounts = successful.map(r => r.result.totalPoints);
281
+ const uniqueTotalPoints = [...new Set(totalPointsCounts)];
282
+
283
+ const numStripsCounts = successful.map(r => r.result.numStrips);
284
+ const uniqueNumStrips = [...new Set(numStripsCounts)];
285
+
286
+ const emptyStripsCounts = successful.map(r => r.result.emptyStrips);
287
+ const uniqueEmptyStrips = [...new Set(emptyStripsCounts)];
288
+
289
+ console.log('\nTotal Points Consistency:');
290
+ if (uniqueTotalPoints.length === 1) {
291
+ console.log(` ✓ All iterations produced ${totalPointsCounts[0].toLocaleString()} points`);
292
+ } else {
293
+ console.log(` ✗ INCONSISTENT! Got ${uniqueTotalPoints.length} different values:`);
294
+ uniqueTotalPoints.forEach(val => {
295
+ const count = totalPointsCounts.filter(v => v === val).length;
296
+ console.log(` ${val.toLocaleString()} points: ${count} times`);
297
+ });
298
+ }
299
+
300
+ console.log('\nEmpty Strips Consistency:');
301
+ if (uniqueEmptyStrips.length === 1) {
302
+ console.log(` ${uniqueEmptyStrips[0] === 0 ? '✓' : '⚠️'} All iterations had ${emptyStripsCounts[0]} empty strips`);
303
+ } else {
304
+ console.log(` ✗ INCONSISTENT! Got ${uniqueEmptyStrips.length} different values:`);
305
+ uniqueEmptyStrips.forEach(val => {
306
+ const count = emptyStripsCounts.filter(v => v === val).length;
307
+ console.log(` ${val} empty strips: ${count} times`);
308
+ });
309
+ }
310
+
311
+ // Timing statistics
312
+ const toolpathTimes = successful.map(r => r.timing.toolpath);
313
+ const avgTime = toolpathTimes.reduce((a, b) => a + b, 0) / toolpathTimes.length;
314
+ const minTime = Math.min(...toolpathTimes);
315
+ const maxTime = Math.max(...toolpathTimes);
316
+ const stdDev = Math.sqrt(
317
+ toolpathTimes.reduce((sum, t) => sum + Math.pow(t - avgTime, 2), 0) / toolpathTimes.length
318
+ );
319
+
320
+ console.log('\nToolpath Timing Statistics:');
321
+ console.log(` Average: ${avgTime.toFixed(1)}ms`);
322
+ console.log(` Min: ${minTime.toFixed(1)}ms`);
323
+ console.log(` Max: ${maxTime.toFixed(1)}ms`);
324
+ console.log(` StdDev: ${stdDev.toFixed(1)}ms (${(stdDev / avgTime * 100).toFixed(1)}%)`);
325
+
326
+ // Overall verdict
327
+ console.log('');
328
+ if (uniqueTotalPoints.length === 1 && uniqueEmptyStrips[0] === 0) {
329
+ console.log('✅ RESULTS ARE CONSISTENT - No non-deterministic behavior detected');
330
+ } else {
331
+ console.log('🛑 RESULTS ARE INCONSISTENT - Non-deterministic behavior detected!');
332
+ console.log(' This suggests workgroups may be getting killed or timing out.');
333
+ }
334
+ }
@@ -0,0 +1,157 @@
1
+ // lathe-cylinder-2-test.cjs
2
+ // Test for lathe-cylinder-2.stl (201 buckets) with bucket batching
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('=== Lathe Cylinder 2 Test (201 buckets) ===');
30
+
31
+ if (!navigator.gpu) {
32
+ return { error: 'WebGPU not available' };
33
+ }
34
+ console.log('✓ WebGPU available');
35
+
36
+ // Import RasterPath
37
+ const { RasterPath } = await import('./raster-path.js');
38
+
39
+ // Load STL files
40
+ console.log('\\nLoading lathe-cylinder-2.stl...');
41
+ const terrainResponse = await fetch('../benchmark/fixtures/lathe-cylinder-2.stl');
42
+ const terrainBuffer = await terrainResponse.arrayBuffer();
43
+
44
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
45
+ const toolBuffer = await toolResponse.arrayBuffer();
46
+
47
+ console.log('✓ Loaded lathe-cylinder-2.stl:', terrainBuffer.byteLength, 'bytes');
48
+ console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
49
+
50
+ // Parse STL files
51
+ function parseBinarySTL(buffer) {
52
+ const dataView = new DataView(buffer);
53
+ const numTriangles = dataView.getUint32(80, true);
54
+ const positions = new Float32Array(numTriangles * 9);
55
+ let offset = 84;
56
+
57
+ for (let i = 0; i < numTriangles; i++) {
58
+ offset += 12; // Skip normal
59
+ for (let j = 0; j < 9; j++) {
60
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
61
+ offset += 4;
62
+ }
63
+ offset += 2; // Skip attribute byte count
64
+ }
65
+ return positions;
66
+ }
67
+
68
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
69
+ const toolTriangles = parseBinarySTL(toolBuffer);
70
+ console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
71
+ console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
72
+
73
+ // Test parameters
74
+ const resolution = 0.5;
75
+ const rotationStep = 5.0;
76
+ const xStep = 1;
77
+ const yStep = 1;
78
+ const zFloor = -50;
79
+
80
+ console.log('\\nTest parameters:');
81
+ console.log(' Resolution:', resolution, 'mm');
82
+ console.log(' Rotation step:', rotationStep, '°');
83
+ console.log(' XY step:', xStep + 'x' + yStep, 'points');
84
+ console.log(' Z floor:', zFloor, 'mm');
85
+
86
+ // Create RasterPath instance for radial mode
87
+ console.log('\\nInitializing RasterPath (radial mode with diagnostic=true)...');
88
+ const raster = new RasterPath({
89
+ mode: 'radial',
90
+ resolution: resolution,
91
+ rotationStep: rotationStep,
92
+ diagnostic: true
93
+ });
94
+ await raster.init();
95
+ console.log('✓ RasterPath initialized');
96
+
97
+ // Load tool
98
+ console.log('\\nLoading tool...');
99
+ const t0 = performance.now();
100
+ const toolData = await raster.loadTool({ triangles: toolTriangles });
101
+ const toolTime = performance.now() - t0;
102
+ console.log('✓ Tool:', toolData.pointCount, 'points in', toolTime.toFixed(1), 'ms');
103
+
104
+ // Load terrain
105
+ console.log('\\nLoading terrain...');
106
+ const t1 = performance.now();
107
+ await raster.loadTerrain({ triangles: terrainTriangles });
108
+ const terrainTime = performance.now() - t1;
109
+ console.log('✓ Terrain loaded in', terrainTime.toFixed(1), 'ms');
110
+
111
+ // Generate toolpaths (this should trigger bucket batching)
112
+ console.log('\\n*** Generating toolpaths (watch for bucket batching messages) ***');
113
+ const t2 = performance.now();
114
+ const toolpaths = await raster.generateToolpaths({
115
+ xStep,
116
+ yStep,
117
+ zFloor
118
+ });
119
+ const toolpathTime = performance.now() - t2;
120
+
121
+ console.log('\\n✓ Generated', toolpaths.length, 'toolpaths in', (toolpathTime / 1000).toFixed(2), 's');
122
+ if (toolpaths.length > 0) {
123
+ console.log(' First toolpath:', toolpaths[0].numScanlines, 'scanlines ×', toolpaths[0].pointsPerLine, 'points');
124
+ }
125
+
126
+ console.log('\\n✅ TEST PASSED - No GPU timeout!');
127
+
128
+ return { success: true };
129
+ })();
130
+ `;
131
+
132
+ try {
133
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
134
+
135
+ if (result && result.error) {
136
+ console.error('❌ Test failed:', result.error);
137
+ app.exit(1);
138
+ } else {
139
+ console.log('\n✅ Test completed successfully');
140
+ app.exit(0);
141
+ }
142
+ } catch (error) {
143
+ console.error('❌ Test execution failed:', error);
144
+ app.exit(1);
145
+ }
146
+ });
147
+
148
+ mainWindow.webContents.on('console-message', (event, level, message) => {
149
+ console.log(message);
150
+ });
151
+ }
152
+
153
+ app.whenReady().then(createWindow);
154
+
155
+ app.on('window-all-closed', () => {
156
+ app.quit();
157
+ });