@gridspace/raster-path 1.0.2 → 1.0.3

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.
@@ -2939,7 +2939,7 @@ self.onmessage = async function(e) {
2939
2939
  // DEBUG: Diagnostic logging (BUILD_ID gets injected during build)
2940
2940
  // Used to trace data flow through radial toolpath pipeline
2941
2941
  if (globalStripIdx === 0 || globalStripIdx === 360) {
2942
- debug.log(`[Worker] 4XFJ410L | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) INPUT terrain first 5 Z values: ${strip.positions.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
2942
+ debug.log(`[Worker] 9P5WQ2V3 | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) INPUT terrain first 5 Z values: ${strip.positions.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
2943
2943
  }
2944
2944
 
2945
2945
  const stripToolpathResult = await runToolpathComputeWithBuffers(
@@ -2955,7 +2955,7 @@ self.onmessage = async function(e) {
2955
2955
 
2956
2956
  // DEBUG: Verify toolpath generation output
2957
2957
  if (globalStripIdx === 0 || globalStripIdx === 360) {
2958
- debug.log(`[Worker] 4XFJ410L | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) OUTPUT toolpath first 5 Z values: ${stripToolpathResult.pathData.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
2958
+ debug.log(`[Worker] 9P5WQ2V3 | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}°) OUTPUT toolpath first 5 Z values: ${stripToolpathResult.pathData.slice(0, 5).map(v => v.toFixed(3)).join(',')}`);
2959
2959
  }
2960
2960
 
2961
2961
  allStripToolpaths.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gridspace/raster-path",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "description": "Terrain and Tool Raster Path Finder using WebGPU",
6
6
  "type": "module",
@@ -11,8 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "build/**/*",
14
- "src/**/*.js",
15
- "src/**/*.wgsl",
14
+ "src/**/*",
16
15
  "scripts/**/*",
17
16
  "README.md",
18
17
  "LICENSE"
@@ -0,0 +1,12 @@
1
+ {
2
+ "headers": [
3
+ {
4
+ "source": "**/*",
5
+ "headers": [
6
+ { "key": "Cache-Control", "value": "no-store, no-cache, must-revalidate, proxy-revalidate" },
7
+ { "key": "Pragma", "value": "no-cache" },
8
+ { "key": "Expires", "value": "0" }
9
+ ]
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,253 @@
1
+ // planar-test.cjs
2
+ // Regression test for planar mode using new RasterPath API
3
+ // Tests: loadTool() + loadTerrain() + generateToolpaths()
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 BASELINE_FILE = path.join(OUTPUT_DIR, 'planar-baseline.json');
11
+ const CURRENT_FILE = path.join(OUTPUT_DIR, 'planar-current.json');
12
+
13
+ if (!fs.existsSync(OUTPUT_DIR)) {
14
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
15
+ }
16
+
17
+ let mainWindow;
18
+
19
+ function createWindow() {
20
+ mainWindow = new BrowserWindow({
21
+ width: 1200,
22
+ height: 800,
23
+ show: false,
24
+ webPreferences: {
25
+ nodeIntegration: false,
26
+ contextIsolation: true,
27
+ enableBlinkFeatures: 'WebGPU',
28
+ }
29
+ });
30
+
31
+ const htmlPath = path.join(__dirname, '../../build/index.html');
32
+ mainWindow.loadFile(htmlPath);
33
+
34
+ mainWindow.webContents.on('did-finish-load', async () => {
35
+ console.log('✓ Page loaded');
36
+
37
+ const testScript = `
38
+ (async function() {
39
+ console.log('=== Planar Mode Regression Test ===');
40
+
41
+ if (!navigator.gpu) {
42
+ return { error: 'WebGPU not available' };
43
+ }
44
+ console.log('✓ WebGPU available');
45
+
46
+ // Import RasterPath
47
+ const { RasterPath } = await import('./raster-path.js');
48
+
49
+ // Load STL files
50
+ console.log('\\nLoading STL files...');
51
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
52
+ const terrainBuffer = await terrainResponse.arrayBuffer();
53
+
54
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
55
+ const toolBuffer = await toolResponse.arrayBuffer();
56
+
57
+ console.log('✓ Loaded terrain.stl:', terrainBuffer.byteLength, 'bytes');
58
+ console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
59
+
60
+ // Parse STL files
61
+ function parseBinarySTL(buffer) {
62
+ const dataView = new DataView(buffer);
63
+ const numTriangles = dataView.getUint32(80, true);
64
+ const positions = new Float32Array(numTriangles * 9);
65
+ let offset = 84;
66
+
67
+ for (let i = 0; i < numTriangles; i++) {
68
+ offset += 12; // Skip normal
69
+ for (let j = 0; j < 9; j++) {
70
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
71
+ offset += 4;
72
+ }
73
+ offset += 2; // Skip attribute byte count
74
+ }
75
+ return positions;
76
+ }
77
+
78
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
79
+ const toolTriangles = parseBinarySTL(toolBuffer);
80
+ console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
81
+ console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
82
+
83
+ // Test parameters
84
+ const resolution = 0.05; // 0.05mm high resolution
85
+ const xStep = 1;
86
+ const yStep = 1;
87
+ const zFloor = -100;
88
+
89
+ console.log('\\nTest parameters:');
90
+ console.log(' Resolution:', resolution, 'mm');
91
+ console.log(' XY step:', xStep + 'x' + yStep, 'points');
92
+ console.log(' Z floor:', zFloor, 'mm');
93
+
94
+ // Create RasterPath instance for planar mode
95
+ console.log('\\nInitializing RasterPath (planar mode)...');
96
+ const raster = new RasterPath({
97
+ mode: 'planar',
98
+ resolution: resolution
99
+ });
100
+ await raster.init();
101
+ console.log('✓ RasterPath initialized');
102
+
103
+ // Load tool (NEW API)
104
+ console.log('\\n1. Loading tool...');
105
+ const t0 = performance.now();
106
+ const toolData = await raster.loadTool({
107
+ triangles: toolTriangles
108
+ });
109
+ const toolTime = performance.now() - t0;
110
+ console.log('✓ Tool:', toolData.pointCount, 'points in', toolTime.toFixed(1), 'ms');
111
+
112
+ // Load terrain (NEW API)
113
+ console.log('\\n2. Loading terrain...');
114
+ const t1 = performance.now();
115
+ const terrainData = await raster.loadTerrain({
116
+ triangles: terrainTriangles,
117
+ zFloor: zFloor
118
+ });
119
+ const terrainTime = performance.now() - t1;
120
+ console.log('✓ Terrain:', terrainData.pointCount, 'points in', terrainTime.toFixed(1), 'ms');
121
+
122
+ // Generate toolpaths (NEW API - no need to pass terrainData/toolData)
123
+ console.log('\\n3. Generating toolpaths...');
124
+ const t2 = performance.now();
125
+ const toolpathData = await raster.generateToolpaths({
126
+ xStep: xStep,
127
+ yStep: yStep,
128
+ zFloor: zFloor
129
+ });
130
+ const toolpathTime = performance.now() - t2;
131
+ console.log('✓ Toolpath:', toolpathData.numScanlines + 'x' + toolpathData.pointsPerLine, '=', toolpathData.pathData.length, 'Z-values');
132
+ console.log(' Generation time:', toolpathTime.toFixed(1), 'ms');
133
+
134
+ // Cleanup
135
+ raster.terminate();
136
+
137
+ // Calculate checksum for regression detection
138
+ let checksum = 0;
139
+ for (let i = 0; i < toolpathData.pathData.length; i++) {
140
+ checksum = (checksum + toolpathData.pathData[i] * (i + 1)) | 0;
141
+ }
142
+
143
+ // Sample first 30 Z-values for debugging
144
+ const sampleSize = Math.min(30, toolpathData.pathData.length);
145
+ const sampleValues = [];
146
+ for (let i = 0; i < sampleSize; i++) {
147
+ sampleValues.push(toolpathData.pathData[i].toFixed(2));
148
+ }
149
+
150
+ return {
151
+ success: true,
152
+ output: {
153
+ parameters: {
154
+ mode: 'planar',
155
+ resolution: resolution,
156
+ xStep,
157
+ yStep,
158
+ zFloor,
159
+ terrainTriangles: terrainTriangles.length / 9,
160
+ toolTriangles: toolTriangles.length / 9
161
+ },
162
+ result: {
163
+ terrainPoints: terrainData.pointCount,
164
+ toolPoints: toolData.pointCount,
165
+ toolpathSize: toolpathData.pathData.length,
166
+ numScanlines: toolpathData.numScanlines,
167
+ pointsPerLine: toolpathData.pointsPerLine,
168
+ checksum: checksum,
169
+ sampleValues: sampleValues
170
+ },
171
+ timing: {
172
+ terrain: terrainTime,
173
+ tool: toolTime,
174
+ toolpath: toolpathTime,
175
+ total: terrainTime + toolTime + toolpathTime
176
+ }
177
+ }
178
+ };
179
+ })();
180
+ `;
181
+
182
+ try {
183
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
184
+
185
+ if (result.error) {
186
+ console.error('❌ Test failed:', result.error);
187
+ app.exit(1);
188
+ return;
189
+ }
190
+
191
+ // Save current output
192
+ const currentData = {
193
+ parameters: result.output.parameters,
194
+ result: result.output.result,
195
+ timing: result.output.timing
196
+ };
197
+
198
+ fs.writeFileSync(CURRENT_FILE, JSON.stringify(currentData, null, 2));
199
+ console.log('\n✓ Saved current output to', CURRENT_FILE);
200
+ console.log(` Toolpath size: ${result.output.result.toolpathSize} Z-values`);
201
+ console.log(` Checksum: ${result.output.result.checksum}`);
202
+ console.log(` Total time: ${result.output.timing.total.toFixed(1)}ms`);
203
+
204
+ // Check if baseline exists
205
+ if (!fs.existsSync(BASELINE_FILE)) {
206
+ console.log('\n📝 No baseline found - saving current as baseline');
207
+ fs.writeFileSync(BASELINE_FILE, JSON.stringify(currentData, null, 2));
208
+ console.log('✅ Baseline created');
209
+ app.exit(0);
210
+ return;
211
+ }
212
+
213
+ // Compare with baseline
214
+ const baseline = JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
215
+
216
+ console.log('\n=== Comparison ===');
217
+ console.log('Baseline checksum:', baseline.result.checksum);
218
+ console.log('Current checksum:', result.output.result.checksum);
219
+
220
+ let passed = true;
221
+
222
+ if (baseline.result.toolpathSize !== result.output.result.toolpathSize) {
223
+ console.error('❌ Toolpath size mismatch!');
224
+ console.error(` Expected: ${baseline.result.toolpathSize}, Got: ${result.output.result.toolpathSize}`);
225
+ passed = false;
226
+ }
227
+
228
+ if (baseline.result.checksum !== result.output.result.checksum) {
229
+ console.error('❌ Checksum mismatch!');
230
+ console.error(` Expected: ${baseline.result.checksum}, Got: ${result.output.result.checksum}`);
231
+ passed = false;
232
+ } else {
233
+ console.log('✓ Checksum matches');
234
+ }
235
+
236
+ if (passed) {
237
+ console.log('\n✅ All checks passed - output matches baseline');
238
+ app.exit(0);
239
+ } else {
240
+ console.log('\n❌ Regression detected - output differs from baseline');
241
+ console.log('To update baseline: cp', CURRENT_FILE, BASELINE_FILE);
242
+ app.exit(1);
243
+ }
244
+
245
+ } catch (error) {
246
+ console.error('Error running test:', error);
247
+ app.exit(1);
248
+ }
249
+ });
250
+ }
251
+
252
+ app.whenReady().then(createWindow);
253
+ app.on('window-all-closed', () => app.quit());
@@ -0,0 +1,230 @@
1
+ // planar-tiling-test.cjs
2
+ // Test for planar tiling with very high resolution (0.01mm)
3
+ // This should trigger automatic tiling to avoid GPU memory allocation failures
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 OUTPUT_FILE = path.join(OUTPUT_DIR, 'planar-tiling-test.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('=== Planar Tiling Test (0.01mm resolution) ===');
39
+
40
+ if (!navigator.gpu) {
41
+ return { error: 'WebGPU not available' };
42
+ }
43
+ console.log('✓ WebGPU available');
44
+
45
+ const { RasterPath } = await import('./raster-path.js');
46
+
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
+ function parseBinarySTL(buffer) {
58
+ const dataView = new DataView(buffer);
59
+ const numTriangles = dataView.getUint32(80, true);
60
+ const positions = new Float32Array(numTriangles * 9);
61
+ let offset = 84;
62
+
63
+ for (let i = 0; i < numTriangles; i++) {
64
+ offset += 12;
65
+ for (let j = 0; j < 9; j++) {
66
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
67
+ offset += 4;
68
+ }
69
+ offset += 2;
70
+ }
71
+ return positions;
72
+ }
73
+
74
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
75
+ const toolTriangles = parseBinarySTL(toolBuffer);
76
+ console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
77
+ console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
78
+
79
+ // HIGH RESOLUTION - should trigger tiling
80
+ const resolution = 0.01;
81
+ const xStep = 1;
82
+ const yStep = 1;
83
+ const zFloor = -100;
84
+
85
+ console.log('\\nTest parameters:');
86
+ console.log(' Resolution:', resolution, 'mm (VERY HIGH - should trigger tiling)');
87
+ console.log(' XY step:', xStep + 'x' + yStep, 'points');
88
+ console.log(' Z floor:', zFloor, 'mm');
89
+
90
+ // Calculate expected grid size
91
+ // Terrain is roughly 1000x1000mm, so at 0.01mm = 100,000 x 100,000 grid
92
+ // = 10 billion points * 4 bytes = 40GB (way over GPU limit!)
93
+ const terrainSize = 1000; // approximate
94
+ const expectedGridSize = Math.ceil(terrainSize / resolution);
95
+ const expectedPoints = expectedGridSize * expectedGridSize;
96
+ const expectedMemoryMB = (expectedPoints * 4) / (1024 * 1024);
97
+ console.log('\\nExpected memory usage:');
98
+ console.log(' Grid size:', expectedGridSize + 'x' + expectedGridSize);
99
+ console.log(' Total points:', (expectedPoints / 1e6).toFixed(1) + 'M');
100
+ console.log(' Memory needed:', expectedMemoryMB.toFixed(0) + 'MB');
101
+ console.log(' GPU limit: ~512MB (should trigger tiling)');
102
+
103
+ console.log('\\nInitializing RasterPath (planar mode)...');
104
+ const raster = new RasterPath({
105
+ mode: 'planar',
106
+ resolution: resolution
107
+ });
108
+ await raster.init();
109
+ console.log('✓ RasterPath initialized');
110
+
111
+ console.log('\\n1. Loading tool (NEW API)...');
112
+ const t0 = performance.now();
113
+ const toolData = await raster.loadTool({
114
+ triangles: toolTriangles
115
+ });
116
+ const toolTime = performance.now() - t0;
117
+ console.log('✓ Tool:', toolData.pointCount, 'points in', toolTime.toFixed(1), 'ms');
118
+
119
+ console.log('\\n2. Loading terrain (NEW API - should use tiling)...');
120
+ const t1 = performance.now();
121
+ let terrainData;
122
+ let terrainTime;
123
+ try {
124
+ terrainData = await raster.loadTerrain({
125
+ triangles: terrainTriangles,
126
+ zFloor: zFloor
127
+ });
128
+ terrainTime = performance.now() - t1;
129
+ console.log('✓ Terrain:', terrainData.pointCount, 'points in', terrainTime.toFixed(1), 'ms');
130
+ } catch (error) {
131
+ console.error('❌ Terrain loading FAILED:', error.message);
132
+ return {
133
+ error: 'Terrain loading failed: ' + error.message,
134
+ expectedTiling: true,
135
+ resolution: resolution
136
+ };
137
+ }
138
+
139
+ console.log('\\n3. Generating toolpaths (NEW API)...');
140
+ const t2 = performance.now();
141
+ const toolpathData = await raster.generateToolpaths({
142
+ xStep: xStep,
143
+ yStep: yStep,
144
+ zFloor: zFloor
145
+ });
146
+ const toolpathTime = performance.now() - t2;
147
+ console.log('✓ Toolpath:', toolpathData.numScanlines + 'x' + toolpathData.pointsPerLine, '=', toolpathData.pathData.length, 'Z-values');
148
+ console.log(' Generation time:', toolpathTime.toFixed(1), 'ms');
149
+
150
+ raster.terminate();
151
+
152
+ // Calculate checksum
153
+ let checksum = 0;
154
+ for (let i = 0; i < Math.min(1000, toolpathData.pathData.length); i++) {
155
+ checksum = (checksum + toolpathData.pathData[i] * (i + 1)) | 0;
156
+ }
157
+
158
+ return {
159
+ success: true,
160
+ tilingWorked: true,
161
+ output: {
162
+ parameters: {
163
+ resolution: resolution,
164
+ expectedMemoryMB: Math.round(expectedMemoryMB)
165
+ },
166
+ result: {
167
+ terrainPoints: terrainData.pointCount,
168
+ toolPoints: toolData.pointCount,
169
+ toolpathSize: toolpathData.pathData.length,
170
+ checksum: checksum
171
+ },
172
+ timing: {
173
+ terrain: terrainTime,
174
+ tool: toolTime,
175
+ toolpath: toolpathTime,
176
+ total: terrainTime + toolTime + toolpathTime
177
+ }
178
+ }
179
+ };
180
+ })();
181
+ `;
182
+
183
+ try {
184
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
185
+
186
+ if (result.error) {
187
+ console.error('\n❌ TEST FAILED - Tiling did not work!');
188
+ console.error('Error:', result.error);
189
+ console.error('Resolution:', result.resolution);
190
+ console.error('Expected tiling:', result.expectedTiling);
191
+
192
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify({
193
+ failed: true,
194
+ error: result.error,
195
+ resolution: result.resolution
196
+ }, null, 2));
197
+
198
+ app.exit(1);
199
+ return;
200
+ }
201
+
202
+ console.log('\n=== Test Complete ===');
203
+ console.log('✅ Tiling worked correctly!');
204
+ console.log('Terrain points:', result.output.result.terrainPoints);
205
+ console.log('Tool points:', result.output.result.toolPoints);
206
+ console.log('Toolpath size:', result.output.result.toolpathSize);
207
+ console.log('Total time:', result.output.timing.total.toFixed(1), 'ms');
208
+
209
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(result.output, null, 2));
210
+ console.log('\n✓ Results written to:', OUTPUT_FILE);
211
+
212
+ app.exit(0);
213
+ } catch (error) {
214
+ console.error('❌ Test execution error:', error);
215
+ app.exit(1);
216
+ }
217
+ });
218
+
219
+ mainWindow.webContents.on('console-message', (event, level, message) => {
220
+ if (message.includes('[Raster') || message.includes('[Worker') || message.includes('Tiling')) {
221
+ console.log('[Browser]', message);
222
+ }
223
+ });
224
+ }
225
+
226
+ app.whenReady().then(createWindow);
227
+
228
+ app.on('window-all-closed', () => {
229
+ app.quit();
230
+ });
@@ -0,0 +1,269 @@
1
+ // radial-test.cjs
2
+ // Regression test for radial mode using new RasterPath API
3
+ // Tests: loadTool() + loadTerrain() + generateToolpaths() with radial projection
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 BASELINE_FILE = path.join(OUTPUT_DIR, 'radial-baseline.json');
11
+ const CURRENT_FILE = path.join(OUTPUT_DIR, 'radial-current.json');
12
+
13
+ if (!fs.existsSync(OUTPUT_DIR)) {
14
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
15
+ }
16
+
17
+ let mainWindow;
18
+
19
+ function createWindow() {
20
+ mainWindow = new BrowserWindow({
21
+ width: 1200,
22
+ height: 800,
23
+ show: false,
24
+ webPreferences: {
25
+ nodeIntegration: false,
26
+ contextIsolation: true,
27
+ enableBlinkFeatures: 'WebGPU',
28
+ }
29
+ });
30
+
31
+ const htmlPath = path.join(__dirname, '../../build/index.html');
32
+ mainWindow.loadFile(htmlPath);
33
+
34
+ mainWindow.webContents.on('did-finish-load', async () => {
35
+ console.log('✓ Page loaded');
36
+
37
+ const testScript = `
38
+ (async function() {
39
+ console.log('=== Radial Mode Regression Test ===');
40
+
41
+ if (!navigator.gpu) {
42
+ return { error: 'WebGPU not available' };
43
+ }
44
+ console.log('✓ WebGPU available');
45
+
46
+ // Import RasterPath
47
+ const { RasterPath } = await import('./raster-path.js');
48
+
49
+ // Load STL files
50
+ console.log('\\nLoading STL files...');
51
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
52
+ const terrainBuffer = await terrainResponse.arrayBuffer();
53
+
54
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
55
+ const toolBuffer = await toolResponse.arrayBuffer();
56
+
57
+ console.log('✓ Loaded terrain.stl:', terrainBuffer.byteLength, 'bytes');
58
+ console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
59
+
60
+ // Parse STL files
61
+ function parseBinarySTL(buffer) {
62
+ const dataView = new DataView(buffer);
63
+ const numTriangles = dataView.getUint32(80, true);
64
+ const positions = new Float32Array(numTriangles * 9);
65
+ let offset = 84;
66
+
67
+ for (let i = 0; i < numTriangles; i++) {
68
+ offset += 12; // Skip normal
69
+ for (let j = 0; j < 9; j++) {
70
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
71
+ offset += 4;
72
+ }
73
+ offset += 2; // Skip attribute byte count
74
+ }
75
+ return positions;
76
+ }
77
+
78
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
79
+ const toolTriangles = parseBinarySTL(toolBuffer);
80
+ console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
81
+ console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
82
+
83
+ // Test parameters
84
+ const resolution = 0.1; // 0.1mm for radial (coarser than planar)
85
+ const rotationStep = 1.0; // 1 degree between rays
86
+ const xStep = 5; // Sample every 5th point
87
+ const yStep = 5;
88
+ const zFloor = 0;
89
+ const radiusOffset = 20; // Tool offset above terrain surface
90
+
91
+ console.log('\\nTest parameters:');
92
+ console.log(' Resolution:', resolution, 'mm');
93
+ console.log(' Rotation step:', rotationStep, '°');
94
+ console.log(' XY step:', xStep + 'x' + yStep, 'points');
95
+ console.log(' Z floor:', zFloor, 'mm');
96
+ console.log(' Radius offset:', radiusOffset, 'mm');
97
+
98
+ // Create RasterPath instance for radial mode
99
+ console.log('\\nInitializing RasterPath (radial mode)...');
100
+ const raster = new RasterPath({
101
+ mode: 'radial',
102
+ resolution: resolution,
103
+ rotationStep: rotationStep
104
+ });
105
+ await raster.init();
106
+ console.log('✓ RasterPath initialized');
107
+
108
+ // Load tool (NEW API)
109
+ console.log('\\n1. Loading tool (NEW API)...');
110
+ const t0 = performance.now();
111
+ const toolData = await raster.loadTool({
112
+ triangles: toolTriangles
113
+ });
114
+ const toolTime = performance.now() - t0;
115
+ console.log('✓ Tool:', toolData.pointCount, 'points in', toolTime.toFixed(1), 'ms');
116
+
117
+ // Load terrain (NEW API - stores triangles for later)
118
+ console.log('\\n2. Loading terrain (NEW API - radial mode)...');
119
+ const t1 = performance.now();
120
+ await raster.loadTerrain({
121
+ triangles: terrainTriangles,
122
+ zFloor: zFloor
123
+ });
124
+ const terrainTime = performance.now() - t1;
125
+ console.log('✓ Terrain loaded (triangles stored, will rasterize during toolpath generation)');
126
+ console.log(' Time:', terrainTime.toFixed(1), 'ms');
127
+
128
+ // Generate toolpaths (NEW API - does rasterization + toolpath generation)
129
+ console.log('\\n3. Generating toolpaths (NEW API - radial)...');
130
+ const t2 = performance.now();
131
+ const toolpathData = await raster.generateToolpaths({
132
+ xStep: xStep,
133
+ yStep: yStep,
134
+ zFloor: zFloor,
135
+ radiusOffset: radiusOffset
136
+ });
137
+ const toolpathTime = performance.now() - t2;
138
+ console.log('✓ Toolpath generated');
139
+ console.log(' Strips:', toolpathData.numStrips);
140
+ console.log(' Total points:', toolpathData.totalPoints);
141
+ console.log(' Generation time:', toolpathTime.toFixed(1), 'ms');
142
+
143
+ // Cleanup
144
+ raster.terminate();
145
+
146
+ // Calculate checksum for regression detection (NEW API - radial mode uses strips)
147
+ let checksum = 0;
148
+ let totalValues = 0;
149
+ for (const strip of toolpathData.strips) {
150
+ for (let i = 0; i < strip.pathData.length; i++) {
151
+ checksum = (checksum + strip.pathData[i] * (totalValues + i + 1)) | 0;
152
+ }
153
+ totalValues += strip.pathData.length;
154
+ }
155
+
156
+ // Sample first 30 Z-values for debugging (from first strip)
157
+ const firstStrip = toolpathData.strips[0];
158
+ const sampleSize = Math.min(30, firstStrip ? firstStrip.pathData.length : 0);
159
+ const sampleValues = [];
160
+ if (firstStrip) {
161
+ for (let i = 0; i < sampleSize; i++) {
162
+ sampleValues.push(firstStrip.pathData[i].toFixed(2));
163
+ }
164
+ }
165
+
166
+ return {
167
+ success: true,
168
+ output: {
169
+ parameters: {
170
+ mode: 'radial',
171
+ resolution: resolution,
172
+ rotationStep: rotationStep,
173
+ xStep,
174
+ yStep,
175
+ zFloor,
176
+ radiusOffset,
177
+ terrainTriangles: terrainTriangles.length / 9,
178
+ toolTriangles: toolTriangles.length / 9
179
+ },
180
+ result: {
181
+ toolPoints: toolData.pointCount,
182
+ numStrips: toolpathData.numStrips,
183
+ totalPoints: toolpathData.totalPoints,
184
+ checksum: checksum,
185
+ sampleValues: sampleValues
186
+ },
187
+ timing: {
188
+ terrain: terrainTime,
189
+ tool: toolTime,
190
+ toolpath: toolpathTime,
191
+ total: terrainTime + toolTime + toolpathTime
192
+ }
193
+ }
194
+ };
195
+ })();
196
+ `;
197
+
198
+ try {
199
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
200
+
201
+ if (result.error) {
202
+ console.error('❌ Test failed:', result.error);
203
+ app.exit(1);
204
+ return;
205
+ }
206
+
207
+ // Save current output
208
+ const currentData = {
209
+ parameters: result.output.parameters,
210
+ result: result.output.result,
211
+ timing: result.output.timing
212
+ };
213
+
214
+ fs.writeFileSync(CURRENT_FILE, JSON.stringify(currentData, null, 2));
215
+ console.log('\n✓ Saved current output to', CURRENT_FILE);
216
+ console.log(` Toolpath size: ${result.output.result.toolpathSize} Z-values`);
217
+ console.log(` Checksum: ${result.output.result.checksum}`);
218
+ console.log(` Total time: ${result.output.timing.total.toFixed(1)}ms`);
219
+
220
+ // Check if baseline exists
221
+ if (!fs.existsSync(BASELINE_FILE)) {
222
+ console.log('\n📝 No baseline found - saving current as baseline');
223
+ fs.writeFileSync(BASELINE_FILE, JSON.stringify(currentData, null, 2));
224
+ console.log('✅ Baseline created');
225
+ app.exit(0);
226
+ return;
227
+ }
228
+
229
+ // Compare with baseline
230
+ const baseline = JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
231
+
232
+ console.log('\n=== Comparison ===');
233
+ console.log('Baseline checksum:', baseline.result.checksum);
234
+ console.log('Current checksum:', result.output.result.checksum);
235
+
236
+ let passed = true;
237
+
238
+ if (baseline.result.toolpathSize !== result.output.result.toolpathSize) {
239
+ console.error('❌ Toolpath size mismatch!');
240
+ console.error(` Expected: ${baseline.result.toolpathSize}, Got: ${result.output.result.toolpathSize}`);
241
+ passed = false;
242
+ }
243
+
244
+ if (baseline.result.checksum !== result.output.result.checksum) {
245
+ console.error('❌ Checksum mismatch!');
246
+ console.error(` Expected: ${baseline.result.checksum}, Got: ${result.output.result.checksum}`);
247
+ passed = false;
248
+ } else {
249
+ console.log('✓ Checksum matches');
250
+ }
251
+
252
+ if (passed) {
253
+ console.log('\n✅ All checks passed - output matches baseline');
254
+ app.exit(0);
255
+ } else {
256
+ console.log('\n❌ Regression detected - output differs from baseline');
257
+ console.log('To update baseline: cp', CURRENT_FILE, BASELINE_FILE);
258
+ app.exit(1);
259
+ }
260
+
261
+ } catch (error) {
262
+ console.error('Error running test:', error);
263
+ app.exit(1);
264
+ }
265
+ });
266
+ }
267
+
268
+ app.whenReady().then(createWindow);
269
+ app.on('window-all-closed', () => app.quit());
@@ -0,0 +1,92 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Raster Path</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="container">
11
+ <!-- Controls Panel -->
12
+ <div class="controls">
13
+ <div class="section">
14
+ <h3>Mode</h3>
15
+ <div class="mode-toggle">
16
+ <label><input type="radio" name="mode" value="planar" checked> Planar</label>
17
+ <label><input type="radio" name="mode" value="radial"> Radial</label>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="section">
22
+ <h3>Load STL</h3>
23
+ <button id="load-model" class="btn">Load Model</button>
24
+ <div id="model-status" class="status">No model loaded</div>
25
+ <button id="load-tool" class="btn">Load Tool</button>
26
+ <div id="tool-status" class="status">No tool loaded</div>
27
+ </div>
28
+
29
+ <div class="section">
30
+ <h3>Resolution</h3>
31
+ <select id="resolution">
32
+ <option value="0.100" selected>0.100mm</option>
33
+ <option value="0.050">0.050mm</option>
34
+ <option value="0.025">0.025mm</option>
35
+ <option value="0.010">0.010mm</option>
36
+ </select>
37
+ </div>
38
+
39
+ <div class="section">
40
+ <h3>Parameters</h3>
41
+ <label>
42
+ Z Floor: <input type="number" id="z-floor" value="-100" step="10" style="width: 70px;">
43
+ </label>
44
+ <label>
45
+ X Step: <input type="number" id="x-step" value="5" min="1" max="50" style="width: 60px;">
46
+ </label>
47
+ <label>
48
+ Y Step: <input type="number" id="y-step" value="5" min="1" max="50" style="width: 60px;">
49
+ </label>
50
+ <label id="angle-step-container" class="hide">
51
+ Angle Step (deg): <input type="number" id="angle-step" value="1" min="0.1" max="10" step="0.1" style="width: 60px;">
52
+ </label>
53
+ </div>
54
+
55
+ <div class="section">
56
+ <h3>Process</h3>
57
+ <button id="rasterize" class="btn" disabled>Rasterize</button>
58
+ <button id="generate-toolpath" class="btn" disabled>Generate Toolpath</button>
59
+ </div>
60
+
61
+ <div class="section">
62
+ <h3>View</h3>
63
+ <label><input type="checkbox" id="show-model" checked> Model</label>
64
+ <label><input type="checkbox" id="show-raster"> Raster</label>
65
+ <label><input type="checkbox" id="show-paths"> Toolpaths</label>
66
+ <label id="wrapped-container" class="hide">
67
+ <input type="checkbox" id="show-wrapped"> Wrapped
68
+ </label>
69
+ </div>
70
+
71
+ <div class="section">
72
+ <h3>Info</h3>
73
+ <div id="info" class="info-text">Ready</div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- 3D Canvas -->
78
+ <canvas id="canvas"></canvas>
79
+ </div>
80
+
81
+ <script type="importmap">
82
+ {
83
+ "imports": {
84
+ "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
85
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
86
+ }
87
+ }
88
+ </script>
89
+ <script type="module" src="app.js"></script>
90
+ <!-- <script type="module" src="webgpu-worker.js"></script> -->
91
+ </body>
92
+ </html>
@@ -0,0 +1,158 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9
+ background: #1a1a1a;
10
+ color: #ffffff;
11
+ overflow: hidden;
12
+ }
13
+
14
+ #container {
15
+ width: 100vw;
16
+ height: 100vh;
17
+ position: relative;
18
+ }
19
+
20
+ #canvas {
21
+ display: block;
22
+ width: 100%;
23
+ height: 100%;
24
+ }
25
+
26
+ .controls {
27
+ position: absolute;
28
+ top: 20px;
29
+ right: 20px;
30
+ background: rgba(0, 0, 0, 0.85);
31
+ backdrop-filter: blur(10px);
32
+ border: 1px solid #333;
33
+ border-radius: 8px;
34
+ padding: 20px;
35
+ min-width: 220px;
36
+ max-width: 280px;
37
+ z-index: 100;
38
+ }
39
+
40
+ .section {
41
+ margin-bottom: 20px;
42
+ padding-bottom: 15px;
43
+ border-bottom: 1px solid #333;
44
+ }
45
+
46
+ .section:last-child {
47
+ border-bottom: none;
48
+ margin-bottom: 0;
49
+ }
50
+
51
+ .section h3 {
52
+ font-size: 13px;
53
+ color: #00ffff;
54
+ margin-bottom: 10px;
55
+ font-weight: 600;
56
+ text-transform: uppercase;
57
+ letter-spacing: 0.5px;
58
+ }
59
+
60
+ .mode-toggle {
61
+ display: flex;
62
+ gap: 12px;
63
+ }
64
+
65
+ .mode-toggle label {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 6px;
69
+ font-size: 13px;
70
+ cursor: pointer;
71
+ }
72
+
73
+ .mode-toggle input[type="radio"] {
74
+ cursor: pointer;
75
+ accent-color: #00ffff;
76
+ }
77
+
78
+ .btn {
79
+ width: 100%;
80
+ padding: 10px;
81
+ margin-bottom: 8px;
82
+ font-size: 13px;
83
+ font-weight: 600;
84
+ background: rgba(0, 255, 255, 0.1);
85
+ border: 1px solid #00ffff;
86
+ border-radius: 6px;
87
+ color: #00ffff;
88
+ cursor: pointer;
89
+ transition: all 0.2s ease;
90
+ }
91
+
92
+ .btn:hover:not(:disabled) {
93
+ background: rgba(0, 255, 255, 0.2);
94
+ box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
95
+ }
96
+
97
+ .btn:active:not(:disabled) {
98
+ background: rgba(0, 255, 255, 0.3);
99
+ transform: translateY(1px);
100
+ }
101
+
102
+ .btn:disabled {
103
+ opacity: 0.3;
104
+ cursor: not-allowed;
105
+ border-color: #666;
106
+ color: #666;
107
+ }
108
+
109
+ .status {
110
+ font-size: 11px;
111
+ color: #888;
112
+ margin-bottom: 12px;
113
+ font-style: italic;
114
+ }
115
+
116
+ select {
117
+ width: 100%;
118
+ padding: 8px;
119
+ font-size: 13px;
120
+ background: rgba(0, 0, 0, 0.6);
121
+ border: 1px solid #00ffff;
122
+ border-radius: 6px;
123
+ color: #ffffff;
124
+ cursor: pointer;
125
+ }
126
+
127
+ select:focus {
128
+ outline: none;
129
+ border-color: #00cccc;
130
+ box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
131
+ }
132
+
133
+ label {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ font-size: 13px;
138
+ margin-bottom: 8px;
139
+ cursor: pointer;
140
+ }
141
+
142
+ input[type="checkbox"] {
143
+ cursor: pointer;
144
+ width: 16px;
145
+ height: 16px;
146
+ accent-color: #00ffff;
147
+ }
148
+
149
+ .info-text {
150
+ font-size: 12px;
151
+ color: #aaa;
152
+ line-height: 1.5;
153
+ white-space: pre-wrap;
154
+ }
155
+
156
+ .hide {
157
+ display: none;
158
+ }