@gridspace/raster-path 1.0.2 → 1.0.4
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/README.md +3 -5
- package/build/app.js +363 -39
- package/build/index.html +39 -1
- package/build/raster-path.js +13 -13
- package/build/style.css +65 -0
- package/build/webgpu-worker.js +475 -686
- package/package.json +7 -4
- package/scripts/build-shaders.js +1 -1
- package/src/etc/serve.json +12 -0
- package/src/index.js +13 -13
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/lathe-cylinder-2-debug.cjs +334 -0
- package/src/test/lathe-cylinder-2-test.cjs +157 -0
- package/src/test/lathe-cylinder-test.cjs +198 -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/test/work-estimation-profile.cjs +406 -0
- package/src/test/workload-calculator-demo.cjs +113 -0
- package/src/test/workload-calibration.cjs +310 -0
- package/src/web/app.js +363 -39
- package/src/web/index.html +130 -0
- package/src/web/style.css +223 -0
- package/src/web/webgpu-worker.js +470 -687
- package/src/workload-calculator.js +318 -0
|
@@ -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,406 @@
|
|
|
1
|
+
// work-estimation-profile.cjs
|
|
2
|
+
// Profile different models to determine work estimation heuristics for optimal batch sizing
|
|
3
|
+
// Tests multiple models at different batch divisors to find inflection points
|
|
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 RESULTS_FILE = path.join(OUTPUT_DIR, 'tool-diameter-scaling.json');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
13
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Tool scale factors to test (baseline is 5mm radius = 10mm diameter)
|
|
17
|
+
// Scale factors: 0.2 → 2mm diameter, 0.4 → 4mm diameter, 0.6 → 6mm diameter, 0.8 → 8mm diameter, 1.0 → 10mm diameter
|
|
18
|
+
// Only test tool scaling with divisor=1 to measure tool diameter impact on toolpath generation
|
|
19
|
+
const TOOL_SCALES = [0.2, 0.4, 0.6, 0.8, 1.0];
|
|
20
|
+
|
|
21
|
+
// Test configurations - use divisor=1 only for tool scaling tests
|
|
22
|
+
const TEST_CONFIGS = [
|
|
23
|
+
{
|
|
24
|
+
name: 'terrain',
|
|
25
|
+
file: '../benchmark/fixtures/terrain.stl',
|
|
26
|
+
resolution: 0.05,
|
|
27
|
+
rotationStep: 1.0,
|
|
28
|
+
divisors: [1], // Only baseline for tool diameter testing
|
|
29
|
+
expectedTriangles: 75586
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'lathe-cylinder',
|
|
33
|
+
file: '../benchmark/fixtures/lathe-cylinder.stl',
|
|
34
|
+
resolution: 0.05,
|
|
35
|
+
rotationStep: 1.0,
|
|
36
|
+
divisors: [1],
|
|
37
|
+
expectedTriangles: 90620
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'lathe-cylinder-2',
|
|
41
|
+
file: '../benchmark/fixtures/lathe-cylinder-2.stl',
|
|
42
|
+
resolution: 0.05,
|
|
43
|
+
rotationStep: 1.0,
|
|
44
|
+
divisors: [1],
|
|
45
|
+
expectedTriangles: 144890
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'lathe-torture',
|
|
49
|
+
file: '../benchmark/fixtures/lathe-torture.stl',
|
|
50
|
+
resolution: 0.05,
|
|
51
|
+
rotationStep: 1.0,
|
|
52
|
+
divisors: [1],
|
|
53
|
+
expectedTriangles: 1491718
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
console.log('=== Work Estimation Profiling with Tool Diameter Scaling ===');
|
|
58
|
+
console.log(`Testing ${TEST_CONFIGS.length} models × ${TOOL_SCALES.length} tool scales`);
|
|
59
|
+
console.log(`Tool diameters: ${TOOL_SCALES.map(s => `${(s * 10).toFixed(1)}mm`).join(', ')}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
let mainWindow;
|
|
63
|
+
let currentConfigIndex = 0;
|
|
64
|
+
let currentDivisorIndex = 0;
|
|
65
|
+
let currentToolScaleIndex = 0;
|
|
66
|
+
const results = [];
|
|
67
|
+
|
|
68
|
+
function createWindow() {
|
|
69
|
+
mainWindow = new BrowserWindow({
|
|
70
|
+
width: 1200,
|
|
71
|
+
height: 800,
|
|
72
|
+
show: false,
|
|
73
|
+
webPreferences: {
|
|
74
|
+
nodeIntegration: false,
|
|
75
|
+
contextIsolation: true,
|
|
76
|
+
enableBlinkFeatures: 'WebGPU',
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
81
|
+
mainWindow.loadFile(htmlPath);
|
|
82
|
+
|
|
83
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
84
|
+
console.log('✓ Page loaded\n');
|
|
85
|
+
await runNextTest();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Capture detailed timing logs
|
|
89
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
90
|
+
if (message.includes('Batch') && message.includes('timing:')) {
|
|
91
|
+
console.log('[TIMING]', message);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function runNextTest() {
|
|
97
|
+
if (currentConfigIndex >= TEST_CONFIGS.length) {
|
|
98
|
+
// All tests complete
|
|
99
|
+
await analyzeResults();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const config = TEST_CONFIGS[currentConfigIndex];
|
|
104
|
+
const divisors = config.divisors;
|
|
105
|
+
|
|
106
|
+
if (currentToolScaleIndex >= TOOL_SCALES.length) {
|
|
107
|
+
// Move to next divisor
|
|
108
|
+
currentDivisorIndex++;
|
|
109
|
+
currentToolScaleIndex = 0;
|
|
110
|
+
await runNextTest();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (currentDivisorIndex >= divisors.length) {
|
|
115
|
+
// Move to next model
|
|
116
|
+
currentConfigIndex++;
|
|
117
|
+
currentDivisorIndex = 0;
|
|
118
|
+
currentToolScaleIndex = 0;
|
|
119
|
+
await runNextTest();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const divisor = divisors[currentDivisorIndex];
|
|
124
|
+
const toolScale = TOOL_SCALES[currentToolScaleIndex];
|
|
125
|
+
const toolDiameter = (toolScale * 10).toFixed(1);
|
|
126
|
+
|
|
127
|
+
console.log(`${'='.repeat(70)}`);
|
|
128
|
+
console.log(`Model: ${config.name} | Divisor: ${divisor} | Tool: ${toolDiameter}mm`);
|
|
129
|
+
console.log('='.repeat(70));
|
|
130
|
+
|
|
131
|
+
const testScript = `
|
|
132
|
+
(async function() {
|
|
133
|
+
const config = ${JSON.stringify(config)};
|
|
134
|
+
const divisor = ${divisor};
|
|
135
|
+
const toolScale = ${toolScale};
|
|
136
|
+
const toolDiameter = ${toolDiameter};
|
|
137
|
+
|
|
138
|
+
if (!navigator.gpu) {
|
|
139
|
+
return { error: 'WebGPU not available' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
143
|
+
|
|
144
|
+
// Load STL files
|
|
145
|
+
const terrainResponse = await fetch('${config.file}');
|
|
146
|
+
const terrainBuffer = await terrainResponse.arrayBuffer();
|
|
147
|
+
|
|
148
|
+
const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
|
|
149
|
+
const toolBuffer = await toolResponse.arrayBuffer();
|
|
150
|
+
|
|
151
|
+
// Parse STL
|
|
152
|
+
function parseBinarySTL(buffer) {
|
|
153
|
+
const dataView = new DataView(buffer);
|
|
154
|
+
const numTriangles = dataView.getUint32(80, true);
|
|
155
|
+
const positions = new Float32Array(numTriangles * 9);
|
|
156
|
+
let offset = 84;
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < numTriangles; i++) {
|
|
159
|
+
offset += 12;
|
|
160
|
+
for (let j = 0; j < 9; j++) {
|
|
161
|
+
positions[i * 9 + j] = dataView.getFloat32(offset, true);
|
|
162
|
+
offset += 4;
|
|
163
|
+
}
|
|
164
|
+
offset += 2;
|
|
165
|
+
}
|
|
166
|
+
return positions;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const terrainTriangles = parseBinarySTL(terrainBuffer);
|
|
170
|
+
let toolTriangles = parseBinarySTL(toolBuffer);
|
|
171
|
+
|
|
172
|
+
// Scale the tool triangles
|
|
173
|
+
if (toolScale !== 1.0) {
|
|
174
|
+
toolTriangles = toolTriangles.map(v => v * toolScale);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const actualTriangleCount = terrainTriangles.length / 9;
|
|
178
|
+
const toolTriangleCount = toolTriangles.length / 9;
|
|
179
|
+
|
|
180
|
+
console.log('Terrain triangles:', actualTriangleCount);
|
|
181
|
+
console.log('Tool triangles:', toolTriangleCount);
|
|
182
|
+
console.log('Tool diameter:', toolDiameter, 'mm (scale:', toolScale, ')');
|
|
183
|
+
|
|
184
|
+
// Create RasterPath instance
|
|
185
|
+
const raster = new RasterPath({
|
|
186
|
+
mode: 'radial',
|
|
187
|
+
resolution: config.resolution,
|
|
188
|
+
rotationStep: config.rotationStep,
|
|
189
|
+
batchDivisor: divisor
|
|
190
|
+
});
|
|
191
|
+
await raster.init();
|
|
192
|
+
|
|
193
|
+
// Load tool
|
|
194
|
+
const t0 = performance.now();
|
|
195
|
+
await raster.loadTool({ triangles: toolTriangles });
|
|
196
|
+
const toolTime = performance.now() - t0;
|
|
197
|
+
|
|
198
|
+
// Load terrain
|
|
199
|
+
const t1 = performance.now();
|
|
200
|
+
await raster.loadTerrain({
|
|
201
|
+
triangles: terrainTriangles,
|
|
202
|
+
zFloor: 0
|
|
203
|
+
});
|
|
204
|
+
const terrainTime = performance.now() - t1;
|
|
205
|
+
|
|
206
|
+
// Generate toolpaths (batching happens here)
|
|
207
|
+
const t2 = performance.now();
|
|
208
|
+
const toolpathData = await raster.generateToolpaths({
|
|
209
|
+
xStep: 1,
|
|
210
|
+
yStep: 1,
|
|
211
|
+
zFloor: 0,
|
|
212
|
+
radiusOffset: 20
|
|
213
|
+
});
|
|
214
|
+
const toolpathTime = performance.now() - t2;
|
|
215
|
+
|
|
216
|
+
raster.terminate();
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
model: config.name,
|
|
221
|
+
divisor: divisor,
|
|
222
|
+
toolScale: toolScale,
|
|
223
|
+
toolDiameter: toolDiameter,
|
|
224
|
+
triangleCount: actualTriangleCount,
|
|
225
|
+
toolTriangleCount: toolTriangleCount,
|
|
226
|
+
timing: {
|
|
227
|
+
tool: toolTime,
|
|
228
|
+
terrain: terrainTime,
|
|
229
|
+
toolpath: toolpathTime,
|
|
230
|
+
total: toolTime + terrainTime + toolpathTime
|
|
231
|
+
},
|
|
232
|
+
result: {
|
|
233
|
+
numStrips: toolpathData.numStrips,
|
|
234
|
+
totalPoints: toolpathData.totalPoints
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
})();
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
242
|
+
|
|
243
|
+
if (result.error) {
|
|
244
|
+
console.error('❌ Test failed:', result.error);
|
|
245
|
+
app.exit(1);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
results.push(result);
|
|
250
|
+
|
|
251
|
+
console.log(`\nResults:`);
|
|
252
|
+
console.log(` Terrain triangles: ${result.triangleCount.toLocaleString()}`);
|
|
253
|
+
console.log(` Tool triangles: ${result.toolTriangleCount}`);
|
|
254
|
+
console.log(` Tool diameter: ${result.toolDiameter}mm`);
|
|
255
|
+
console.log(` Toolpath time: ${result.timing.toolpath.toFixed(1)}ms`);
|
|
256
|
+
console.log(` Total time: ${result.timing.total.toFixed(1)}ms`);
|
|
257
|
+
console.log(` Strips generated: ${result.result.numStrips}`);
|
|
258
|
+
|
|
259
|
+
// Move to next test
|
|
260
|
+
currentToolScaleIndex++;
|
|
261
|
+
setTimeout(() => runNextTest(), 500);
|
|
262
|
+
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('Error running test:', error);
|
|
265
|
+
app.exit(1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function analyzeResults() {
|
|
270
|
+
console.log('\n' + '='.repeat(70));
|
|
271
|
+
console.log('WORK ESTIMATION ANALYSIS');
|
|
272
|
+
console.log('='.repeat(70));
|
|
273
|
+
|
|
274
|
+
// Save raw results
|
|
275
|
+
const resultsData = {
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
configs: TEST_CONFIGS,
|
|
278
|
+
results: results
|
|
279
|
+
};
|
|
280
|
+
fs.writeFileSync(RESULTS_FILE, JSON.stringify(resultsData, null, 2));
|
|
281
|
+
console.log(`\n✓ Raw results saved to: ${RESULTS_FILE}\n`);
|
|
282
|
+
|
|
283
|
+
// Group by model
|
|
284
|
+
const modelGroups = {};
|
|
285
|
+
for (const result of results) {
|
|
286
|
+
if (!modelGroups[result.model]) {
|
|
287
|
+
modelGroups[result.model] = [];
|
|
288
|
+
}
|
|
289
|
+
modelGroups[result.model].push(result);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Analyze each model - focus on tool diameter impact
|
|
293
|
+
console.log('--- Tool Diameter Impact Analysis ---\n');
|
|
294
|
+
|
|
295
|
+
const toolDiameterData = [];
|
|
296
|
+
|
|
297
|
+
for (const [modelName, modelResults] of Object.entries(modelGroups)) {
|
|
298
|
+
console.log(`\n${modelName.toUpperCase()}`);
|
|
299
|
+
console.log('─'.repeat(70));
|
|
300
|
+
|
|
301
|
+
const triangleCount = modelResults[0].triangleCount;
|
|
302
|
+
const totalAngles = modelResults[0].result.numStrips; // 360 for 1° step
|
|
303
|
+
|
|
304
|
+
console.log(`Triangle count: ${triangleCount.toLocaleString()}`);
|
|
305
|
+
console.log(`Total angles: ${totalAngles}`);
|
|
306
|
+
console.log(`Resolution: 0.05mm\n`);
|
|
307
|
+
|
|
308
|
+
console.log('Tool Ø | Tool Tris | Toolpath (ms) | Total (ms) | Toolpath/Strip');
|
|
309
|
+
console.log('-------|-----------|---------------|------------|---------------');
|
|
310
|
+
|
|
311
|
+
const baseline = modelResults[0]; // Smallest tool
|
|
312
|
+
|
|
313
|
+
for (const result of modelResults) {
|
|
314
|
+
const toolpathPerStrip = result.timing.toolpath / totalAngles;
|
|
315
|
+
const speedup = baseline.timing.toolpath / result.timing.toolpath;
|
|
316
|
+
|
|
317
|
+
console.log(
|
|
318
|
+
`${String(result.toolDiameter + 'mm').padEnd(6)} | ` +
|
|
319
|
+
`${String(result.toolTriangleCount).padStart(9)} | ` +
|
|
320
|
+
`${result.timing.toolpath.toFixed(1).padStart(13)} | ` +
|
|
321
|
+
`${result.timing.total.toFixed(1).padStart(10)} | ` +
|
|
322
|
+
`${toolpathPerStrip.toFixed(2).padStart(14)}ms`
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Store data for correlation analysis
|
|
326
|
+
toolDiameterData.push({
|
|
327
|
+
model: modelName,
|
|
328
|
+
triangleCount: triangleCount,
|
|
329
|
+
toolDiameter: result.toolDiameter,
|
|
330
|
+
toolTriangleCount: result.toolTriangleCount,
|
|
331
|
+
toolpathTime: result.timing.toolpath,
|
|
332
|
+
toolpathPerStrip: toolpathPerStrip
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log('');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Correlation analysis - tool diameter vs toolpath time
|
|
340
|
+
console.log('\n' + '='.repeat(70));
|
|
341
|
+
console.log('TOOL DIAMETER CORRELATION ANALYSIS');
|
|
342
|
+
console.log('='.repeat(70));
|
|
343
|
+
|
|
344
|
+
// Group by model to analyze scaling
|
|
345
|
+
const modelToolData = {};
|
|
346
|
+
for (const data of toolDiameterData) {
|
|
347
|
+
if (!modelToolData[data.model]) {
|
|
348
|
+
modelToolData[data.model] = [];
|
|
349
|
+
}
|
|
350
|
+
modelToolData[data.model].push(data);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log('\n--- Scaling Relationship Analysis ---\n');
|
|
354
|
+
|
|
355
|
+
for (const [modelName, dataPoints] of Object.entries(modelToolData)) {
|
|
356
|
+
console.log(`${modelName.toUpperCase()}:`);
|
|
357
|
+
|
|
358
|
+
// Sort by tool diameter
|
|
359
|
+
dataPoints.sort((a, b) => a.toolDiameter - b.toolDiameter);
|
|
360
|
+
|
|
361
|
+
const baseline = dataPoints[0];
|
|
362
|
+
|
|
363
|
+
console.log(` Tool Ø vs Time (normalized to ${baseline.toolDiameter}mm baseline):`);
|
|
364
|
+
|
|
365
|
+
for (const point of dataPoints) {
|
|
366
|
+
const diameterRatio = point.toolDiameter / baseline.toolDiameter;
|
|
367
|
+
const timeRatio = point.toolpathTime / baseline.toolpathTime;
|
|
368
|
+
const expectedLinear = diameterRatio;
|
|
369
|
+
const expectedSquare = diameterRatio ** 2;
|
|
370
|
+
|
|
371
|
+
console.log(` ${point.toolDiameter.toFixed(1)}mm: ${timeRatio.toFixed(2)}x slower`);
|
|
372
|
+
console.log(` Diameter ratio: ${diameterRatio.toFixed(2)}x`);
|
|
373
|
+
console.log(` If linear (Ø): ${expectedLinear.toFixed(2)}x (error: ${Math.abs(timeRatio - expectedLinear).toFixed(2)})`);
|
|
374
|
+
console.log(` If square (ز): ${expectedSquare.toFixed(2)}x (error: ${Math.abs(timeRatio - expectedSquare).toFixed(2)})`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Calculate correlation coefficient for linear and square relationships
|
|
378
|
+
const diameterRatios = dataPoints.map(p => p.toolDiameter / baseline.toolDiameter);
|
|
379
|
+
const timeRatios = dataPoints.map(p => p.toolpathTime / baseline.toolpathTime);
|
|
380
|
+
|
|
381
|
+
// Linear correlation: timeRatio ~ diameterRatio
|
|
382
|
+
const linearErrors = diameterRatios.map((dr, i) => Math.abs(timeRatios[i] - dr));
|
|
383
|
+
const avgLinearError = linearErrors.reduce((sum, e) => sum + e, 0) / linearErrors.length;
|
|
384
|
+
|
|
385
|
+
// Square correlation: timeRatio ~ diameterRatio²
|
|
386
|
+
const squareErrors = diameterRatios.map((dr, i) => Math.abs(timeRatios[i] - dr ** 2));
|
|
387
|
+
const avgSquareError = squareErrors.reduce((sum, e) => sum + e, 0) / squareErrors.length;
|
|
388
|
+
|
|
389
|
+
console.log(` Average error: Linear ${avgLinearError.toFixed(3)}, Square ${avgSquareError.toFixed(3)}`);
|
|
390
|
+
console.log(` ${avgSquareError < avgLinearError ? '✓ Square relationship fits better' : '✓ Linear relationship fits better'}\n`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Summary
|
|
394
|
+
console.log('\n--- Summary ---\n');
|
|
395
|
+
console.log('This test measures the relationship between tool diameter and toolpath');
|
|
396
|
+
console.log('generation time. The hypothesis is that work scales with tool diameter²');
|
|
397
|
+
console.log('because the sparse tool representation has more points to check.');
|
|
398
|
+
console.log('\nThe analysis compares linear (Ø) vs square (ز) scaling to validate');
|
|
399
|
+
console.log('or refute this hypothesis.');
|
|
400
|
+
|
|
401
|
+
console.log('\n✅ Profiling complete!');
|
|
402
|
+
app.exit(0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
app.whenReady().then(createWindow);
|
|
406
|
+
app.on('window-all-closed', () => app.quit());
|