@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.
@@ -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());
@@ -0,0 +1,113 @@
1
+ // workload-calculator-demo.cjs
2
+ // Demonstration of the workload calculator
3
+
4
+ const { WorkloadCalculator } = require('../workload-calculator.js');
5
+
6
+ const calculator = new WorkloadCalculator();
7
+
8
+ console.log('=== Workload Calculator Demo ===\n');
9
+
10
+ // Example 1: Typical lathe part
11
+ console.log('Example 1: Lathe cylinder (typical parameters)');
12
+ calculator.printAnalysis({
13
+ triangleCount: 90620,
14
+ bounds: {
15
+ minX: 0, maxX: 100,
16
+ minY: -25, maxY: 25,
17
+ minZ: -25, maxZ: 25
18
+ },
19
+ resolution: 0.05,
20
+ rotationStep: 1.0,
21
+ toolDiameter: 2.5
22
+ });
23
+
24
+ console.log('\n' + '='.repeat(70) + '\n');
25
+
26
+ // Example 2: High resolution
27
+ console.log('Example 2: Same part, high resolution (0.01mm)');
28
+ calculator.printAnalysis({
29
+ triangleCount: 90620,
30
+ bounds: {
31
+ minX: 0, maxX: 100,
32
+ minY: -25, maxY: 25,
33
+ minZ: -25, maxZ: 25
34
+ },
35
+ resolution: 0.01,
36
+ rotationStep: 1.0,
37
+ toolDiameter: 2.5
38
+ });
39
+
40
+ console.log('\n' + '='.repeat(70) + '\n');
41
+
42
+ // Example 3: Large tool
43
+ console.log('Example 3: Same part, larger tool (5mm)');
44
+ calculator.printAnalysis({
45
+ triangleCount: 90620,
46
+ bounds: {
47
+ minX: 0, maxX: 100,
48
+ minY: -25, maxY: 25,
49
+ minZ: -25, maxZ: 25
50
+ },
51
+ resolution: 0.05,
52
+ rotationStep: 1.0,
53
+ toolDiameter: 5.0
54
+ });
55
+
56
+ console.log('\n' + '='.repeat(70) + '\n');
57
+
58
+ // Example 4: Fine angular step
59
+ console.log('Example 4: Same part, fine angular step (0.5°)');
60
+ calculator.printAnalysis({
61
+ triangleCount: 90620,
62
+ bounds: {
63
+ minX: 0, maxX: 100,
64
+ minY: -25, maxY: 25,
65
+ minZ: -25, maxZ: 25
66
+ },
67
+ resolution: 0.05,
68
+ rotationStep: 0.5,
69
+ toolDiameter: 2.5
70
+ });
71
+
72
+ console.log('\n' + '='.repeat(70) + '\n');
73
+
74
+ // Example 5: Complex torture test
75
+ console.log('Example 5: Lathe torture (1.5M triangles)');
76
+ calculator.printAnalysis({
77
+ triangleCount: 1491718,
78
+ bounds: {
79
+ minX: 0, maxX: 100,
80
+ minY: -30, maxY: 30,
81
+ minZ: -30, maxZ: 30
82
+ },
83
+ resolution: 0.05,
84
+ rotationStep: 1.0,
85
+ toolDiameter: 2.5
86
+ });
87
+
88
+ console.log('\n' + '='.repeat(70) + '\n');
89
+
90
+ // Example 6: Memory constraint check
91
+ console.log('Example 6: Memory limit suggestions');
92
+ const params = {
93
+ triangleCount: 90620,
94
+ bounds: {
95
+ minX: 0, maxX: 100,
96
+ minY: -25, maxY: 25,
97
+ minZ: -25, maxZ: 25
98
+ },
99
+ resolution: 0.01, // Very fine - may exceed memory
100
+ rotationStep: 0.5,
101
+ toolDiameter: 2.5
102
+ };
103
+
104
+ const suggestion = calculator.suggestOptimalParameters(params, 256);
105
+ console.log(suggestion.message);
106
+ if (suggestion.needsAdjustment) {
107
+ console.log(`Current memory: ${suggestion.currentMemory.toFixed(1)} MB`);
108
+ console.log('Suggestions:');
109
+ console.log(` Resolution: ${suggestion.suggestions.resolution}mm`);
110
+ console.log(` Angular step: ${suggestion.suggestions.angularStep}°`);
111
+ }
112
+
113
+ console.log('\n=== Demo Complete ===');