@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.
- package/build/webgpu-worker.js +2 -2
- package/package.json +2 -3
- package/src/etc/serve.json +12 -0
- package/src/test/planar-test.cjs +253 -0
- package/src/test/planar-tiling-test.cjs +230 -0
- package/src/test/radial-test.cjs +269 -0
- package/src/web/index.html +92 -0
- package/src/web/style.css +158 -0
package/build/webgpu-worker.js
CHANGED
|
@@ -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]
|
|
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]
|
|
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.
|
|
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
|
|
15
|
-
"src/**/*.wgsl",
|
|
14
|
+
"src/**/*",
|
|
16
15
|
"scripts/**/*",
|
|
17
16
|
"README.md",
|
|
18
17
|
"LICENSE"
|
|
@@ -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
|
+
}
|