@gridspace/raster-path 1.0.3 → 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 +6 -2
- package/scripts/build-shaders.js +1 -1
- 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/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 +39 -1
- package/src/web/style.css +65 -0
- package/src/web/webgpu-worker.js +470 -687
- package/src/workload-calculator.js +318 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// workload-calibration.cjs
|
|
2
|
+
// Comprehensive test matrix to calibrate workload estimation formulas
|
|
3
|
+
// Tests: resolution × tool diameter × angular step × model
|
|
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, 'workload-calibration.json');
|
|
11
|
+
const CSV_FILE = path.join(OUTPUT_DIR, 'workload-calibration.csv');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
14
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Test matrix dimensions
|
|
18
|
+
const RESOLUTIONS = [0.05, 0.04, 0.03, 0.02, 0.01];
|
|
19
|
+
const TOOL_SCALES = [0.1, 0.2, 0.3, 0.4, 0.5]; // 1mm, 2mm, 3mm, 4mm, 5mm (base tool is 10mm)
|
|
20
|
+
const ANGULAR_STEPS = [2.0, 1.0, 0.5];
|
|
21
|
+
|
|
22
|
+
// Find all lathe-*.stl models
|
|
23
|
+
const MODELS = [
|
|
24
|
+
{ name: 'lathe-cylinder', file: '../benchmark/fixtures/lathe-cylinder.stl' },
|
|
25
|
+
{ name: 'lathe-cylinder-2', file: '../benchmark/fixtures/lathe-cylinder-2.stl' },
|
|
26
|
+
{ name: 'lathe-puck', file: '../benchmark/fixtures/lathe-puck.stl' },
|
|
27
|
+
{ name: 'lathe-torture', file: '../benchmark/fixtures/lathe-torture.stl' }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const totalTests = MODELS.length * RESOLUTIONS.length * TOOL_SCALES.length * ANGULAR_STEPS.length;
|
|
31
|
+
|
|
32
|
+
console.log('=== Workload Calibration Test Matrix ===');
|
|
33
|
+
console.log(`Models: ${MODELS.length} (${MODELS.map(m => m.name).join(', ')})`);
|
|
34
|
+
console.log(`Resolutions: ${RESOLUTIONS.length} (${RESOLUTIONS.join(', ')})`);
|
|
35
|
+
console.log(`Tool sizes: ${TOOL_SCALES.length} (${TOOL_SCALES.map(s => (s * 10).toFixed(1) + 'mm').join(', ')})`);
|
|
36
|
+
console.log(`Angular steps: ${ANGULAR_STEPS.length} (${ANGULAR_STEPS.join('°, ')}°)`);
|
|
37
|
+
console.log(`Total tests: ${totalTests}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
let mainWindow;
|
|
41
|
+
let currentModelIndex = 0;
|
|
42
|
+
let currentResolutionIndex = 0;
|
|
43
|
+
let currentToolScaleIndex = 0;
|
|
44
|
+
let currentAngularStepIndex = 0;
|
|
45
|
+
const results = [];
|
|
46
|
+
let testNumber = 0;
|
|
47
|
+
|
|
48
|
+
function createWindow() {
|
|
49
|
+
mainWindow = new BrowserWindow({
|
|
50
|
+
width: 1200,
|
|
51
|
+
height: 800,
|
|
52
|
+
show: false,
|
|
53
|
+
webPreferences: {
|
|
54
|
+
nodeIntegration: false,
|
|
55
|
+
contextIsolation: true,
|
|
56
|
+
enableBlinkFeatures: 'WebGPU',
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
61
|
+
mainWindow.loadFile(htmlPath);
|
|
62
|
+
|
|
63
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
64
|
+
console.log('✓ Page loaded\n');
|
|
65
|
+
await runNextTest();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runNextTest() {
|
|
70
|
+
// Check if all tests complete
|
|
71
|
+
if (currentModelIndex >= MODELS.length) {
|
|
72
|
+
await analyzeResults();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Advance through dimensions
|
|
77
|
+
if (currentAngularStepIndex >= ANGULAR_STEPS.length) {
|
|
78
|
+
currentToolScaleIndex++;
|
|
79
|
+
currentAngularStepIndex = 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentToolScaleIndex >= TOOL_SCALES.length) {
|
|
83
|
+
currentResolutionIndex++;
|
|
84
|
+
currentToolScaleIndex = 0;
|
|
85
|
+
currentAngularStepIndex = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (currentResolutionIndex >= RESOLUTIONS.length) {
|
|
89
|
+
currentModelIndex++;
|
|
90
|
+
currentResolutionIndex = 0;
|
|
91
|
+
currentToolScaleIndex = 0;
|
|
92
|
+
currentAngularStepIndex = 0;
|
|
93
|
+
await runNextTest();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const model = MODELS[currentModelIndex];
|
|
98
|
+
const resolution = RESOLUTIONS[currentResolutionIndex];
|
|
99
|
+
const toolScale = TOOL_SCALES[currentToolScaleIndex];
|
|
100
|
+
const angularStep = ANGULAR_STEPS[currentAngularStepIndex];
|
|
101
|
+
const toolDiameter = (toolScale * 10).toFixed(1);
|
|
102
|
+
|
|
103
|
+
testNumber++;
|
|
104
|
+
const progress = `[${testNumber}/${totalTests}]`;
|
|
105
|
+
|
|
106
|
+
console.log(`${progress} ${model.name} | res=${resolution} | tool=${toolDiameter}mm | step=${angularStep}°`);
|
|
107
|
+
|
|
108
|
+
const testScript = `
|
|
109
|
+
(async function() {
|
|
110
|
+
const modelFile = '${model.file}';
|
|
111
|
+
const resolution = ${resolution};
|
|
112
|
+
const toolScale = ${toolScale};
|
|
113
|
+
const angularStep = ${angularStep};
|
|
114
|
+
|
|
115
|
+
if (!navigator.gpu) {
|
|
116
|
+
return { error: 'WebGPU not available' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
120
|
+
|
|
121
|
+
// Load STL files
|
|
122
|
+
const terrainResponse = await fetch(modelFile);
|
|
123
|
+
const terrainBuffer = await terrainResponse.arrayBuffer();
|
|
124
|
+
|
|
125
|
+
const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
|
|
126
|
+
const toolBuffer = await toolResponse.arrayBuffer();
|
|
127
|
+
|
|
128
|
+
// Parse STL
|
|
129
|
+
function parseBinarySTL(buffer) {
|
|
130
|
+
const dataView = new DataView(buffer);
|
|
131
|
+
const numTriangles = dataView.getUint32(80, true);
|
|
132
|
+
const positions = new Float32Array(numTriangles * 9);
|
|
133
|
+
let offset = 84;
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < numTriangles; i++) {
|
|
136
|
+
offset += 12;
|
|
137
|
+
for (let j = 0; j < 9; j++) {
|
|
138
|
+
positions[i * 9 + j] = dataView.getFloat32(offset, true);
|
|
139
|
+
offset += 4;
|
|
140
|
+
}
|
|
141
|
+
offset += 2;
|
|
142
|
+
}
|
|
143
|
+
return positions;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const terrainTriangles = parseBinarySTL(terrainBuffer);
|
|
147
|
+
let toolTriangles = parseBinarySTL(toolBuffer);
|
|
148
|
+
|
|
149
|
+
// Scale the tool
|
|
150
|
+
if (toolScale !== 1.0) {
|
|
151
|
+
toolTriangles = toolTriangles.map(v => v * toolScale);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const triangleCount = terrainTriangles.length / 9;
|
|
155
|
+
const toolTriangleCount = toolTriangles.length / 9;
|
|
156
|
+
|
|
157
|
+
// Create RasterPath instance
|
|
158
|
+
const raster = new RasterPath({
|
|
159
|
+
mode: 'radial',
|
|
160
|
+
resolution: resolution,
|
|
161
|
+
rotationStep: angularStep
|
|
162
|
+
});
|
|
163
|
+
await raster.init();
|
|
164
|
+
|
|
165
|
+
// Load tool
|
|
166
|
+
const t0 = performance.now();
|
|
167
|
+
await raster.loadTool({ triangles: toolTriangles });
|
|
168
|
+
const toolTime = performance.now() - t0;
|
|
169
|
+
|
|
170
|
+
// Load terrain
|
|
171
|
+
const t1 = performance.now();
|
|
172
|
+
await raster.loadTerrain({
|
|
173
|
+
triangles: terrainTriangles,
|
|
174
|
+
zFloor: 0
|
|
175
|
+
});
|
|
176
|
+
const terrainTime = performance.now() - t1;
|
|
177
|
+
|
|
178
|
+
// Generate toolpaths
|
|
179
|
+
const t2 = performance.now();
|
|
180
|
+
const toolpathData = await raster.generateToolpaths({
|
|
181
|
+
xStep: 1,
|
|
182
|
+
yStep: 1,
|
|
183
|
+
zFloor: 0,
|
|
184
|
+
radiusOffset: 20
|
|
185
|
+
});
|
|
186
|
+
const toolpathTime = performance.now() - t2;
|
|
187
|
+
|
|
188
|
+
raster.terminate();
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
success: true,
|
|
192
|
+
triangleCount: triangleCount,
|
|
193
|
+
toolTriangleCount: toolTriangleCount,
|
|
194
|
+
timing: {
|
|
195
|
+
tool: toolTime,
|
|
196
|
+
terrain: terrainTime,
|
|
197
|
+
toolpath: toolpathTime,
|
|
198
|
+
total: toolTime + terrainTime + toolpathTime
|
|
199
|
+
},
|
|
200
|
+
result: {
|
|
201
|
+
numStrips: toolpathData.numStrips,
|
|
202
|
+
totalPoints: toolpathData.totalPoints
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
})();
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
210
|
+
|
|
211
|
+
if (result.error) {
|
|
212
|
+
console.error('❌ Test failed:', result.error);
|
|
213
|
+
app.exit(1);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Store result with test parameters
|
|
218
|
+
results.push({
|
|
219
|
+
model: model.name,
|
|
220
|
+
resolution: resolution,
|
|
221
|
+
toolDiameter: parseFloat(toolDiameter),
|
|
222
|
+
angularStep: angularStep,
|
|
223
|
+
triangleCount: result.triangleCount,
|
|
224
|
+
toolTriangleCount: result.toolTriangleCount,
|
|
225
|
+
numStrips: result.result.numStrips,
|
|
226
|
+
timing: result.timing
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
console.log(` ✓ Toolpath: ${result.timing.toolpath.toFixed(0)}ms | Total: ${result.timing.total.toFixed(0)}ms\n`);
|
|
230
|
+
|
|
231
|
+
// Move to next test
|
|
232
|
+
currentAngularStepIndex++;
|
|
233
|
+
setTimeout(() => runNextTest(), 100);
|
|
234
|
+
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Error running test:', error);
|
|
237
|
+
app.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function analyzeResults() {
|
|
242
|
+
console.log('\n' + '='.repeat(70));
|
|
243
|
+
console.log('WORKLOAD CALIBRATION ANALYSIS');
|
|
244
|
+
console.log('='.repeat(70));
|
|
245
|
+
|
|
246
|
+
// Save raw results as JSON
|
|
247
|
+
const resultsData = {
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
testMatrix: {
|
|
250
|
+
models: MODELS.map(m => m.name),
|
|
251
|
+
resolutions: RESOLUTIONS,
|
|
252
|
+
toolDiameters: TOOL_SCALES.map(s => s * 10),
|
|
253
|
+
angularSteps: ANGULAR_STEPS
|
|
254
|
+
},
|
|
255
|
+
totalTests: totalTests,
|
|
256
|
+
results: results
|
|
257
|
+
};
|
|
258
|
+
fs.writeFileSync(RESULTS_FILE, JSON.stringify(resultsData, null, 2));
|
|
259
|
+
console.log(`\n✓ Raw JSON saved to: ${RESULTS_FILE}`);
|
|
260
|
+
|
|
261
|
+
// Generate CSV for easy analysis
|
|
262
|
+
const csvHeader = 'model,resolution,toolDiameter,angularStep,triangleCount,numStrips,toolTime,terrainTime,toolpathTime,totalTime\n';
|
|
263
|
+
const csvRows = results.map(r =>
|
|
264
|
+
`${r.model},${r.resolution},${r.toolDiameter},${r.angularStep},${r.triangleCount},${r.numStrips},` +
|
|
265
|
+
`${r.timing.tool.toFixed(2)},${r.timing.terrain.toFixed(2)},${r.timing.toolpath.toFixed(2)},${r.timing.total.toFixed(2)}`
|
|
266
|
+
).join('\n');
|
|
267
|
+
fs.writeFileSync(CSV_FILE, csvHeader + csvRows);
|
|
268
|
+
console.log(`✓ CSV saved to: ${CSV_FILE}\n`);
|
|
269
|
+
|
|
270
|
+
// Summary statistics
|
|
271
|
+
console.log('--- Summary Statistics ---\n');
|
|
272
|
+
|
|
273
|
+
// Group by model
|
|
274
|
+
const modelGroups = {};
|
|
275
|
+
for (const result of results) {
|
|
276
|
+
if (!modelGroups[result.model]) {
|
|
277
|
+
modelGroups[result.model] = [];
|
|
278
|
+
}
|
|
279
|
+
modelGroups[result.model].push(result);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const [modelName, modelResults] of Object.entries(modelGroups)) {
|
|
283
|
+
const times = modelResults.map(r => r.timing.toolpath);
|
|
284
|
+
const avgTime = times.reduce((sum, t) => sum + t, 0) / times.length;
|
|
285
|
+
const minTime = Math.min(...times);
|
|
286
|
+
const maxTime = Math.max(...times);
|
|
287
|
+
|
|
288
|
+
console.log(`${modelName}:`);
|
|
289
|
+
console.log(` Tests: ${modelResults.length}`);
|
|
290
|
+
console.log(` Toolpath time: min=${minTime.toFixed(0)}ms, max=${maxTime.toFixed(0)}ms, avg=${avgTime.toFixed(0)}ms`);
|
|
291
|
+
console.log(` Range: ${(maxTime / minTime).toFixed(1)}x variation\n`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Correlation hints
|
|
295
|
+
console.log('--- Key Observations ---\n');
|
|
296
|
+
console.log('The CSV file contains all test data for correlation analysis.');
|
|
297
|
+
console.log('Use it to validate workload formulas by comparing predicted vs actual times.');
|
|
298
|
+
console.log('\nVariables available for formula calibration:');
|
|
299
|
+
console.log(' - resolution (inverse square relationship expected)');
|
|
300
|
+
console.log(' - toolDiameter (linear relationship confirmed)');
|
|
301
|
+
console.log(' - angularStep (inverse relationship expected)');
|
|
302
|
+
console.log(' - triangleCount (linear or density-based relationship)');
|
|
303
|
+
console.log(' - numStrips (360 / angularStep)');
|
|
304
|
+
|
|
305
|
+
console.log('\n✅ Calibration complete!');
|
|
306
|
+
app.exit(0);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
app.whenReady().then(createWindow);
|
|
310
|
+
app.on('window-all-closed', () => app.quit());
|