@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.
@@ -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());