@gridspace/raster-path 1.0.3 → 1.0.5
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 +40 -2
- package/build/raster-path.js +16 -24
- package/build/raster-worker.js +2450 -0
- package/build/style.css +65 -0
- package/package.json +12 -4
- package/scripts/build-shaders.js +32 -8
- package/src/core/path-planar.js +788 -0
- package/src/core/path-radial.js +651 -0
- package/src/core/raster-config.js +185 -0
- package/src/{index.js → core/raster-path.js} +16 -24
- package/src/core/raster-planar.js +754 -0
- package/src/core/raster-tool.js +104 -0
- package/src/core/raster-worker.js +152 -0
- package/src/core/workload-calibrate.js +416 -0
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/shaders/workload-calibrate.wgsl +106 -0
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/calibrate-test.cjs +136 -0
- package/src/test/extreme-work-test.cjs +167 -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/radial-thread-limit-test.cjs +152 -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 +40 -2
- package/src/web/style.css +65 -0
- package/src/workload-calculator.js +318 -0
- package/build/webgpu-worker.js +0 -3011
- package/src/web/webgpu-worker.js +0 -2520
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const { app, BrowserWindow } = require('electron');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
let testPassed = false;
|
|
6
|
+
|
|
7
|
+
app.on('ready', async () => {
|
|
8
|
+
const win = new BrowserWindow({
|
|
9
|
+
width: 800,
|
|
10
|
+
height: 600,
|
|
11
|
+
show: false,
|
|
12
|
+
webPreferences: {
|
|
13
|
+
nodeIntegration: false,
|
|
14
|
+
contextIsolation: true
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Load test page
|
|
19
|
+
await win.loadFile(path.join(__dirname, '../../build/index.html'));
|
|
20
|
+
|
|
21
|
+
// Inject test script
|
|
22
|
+
await win.webContents.executeJavaScript(`
|
|
23
|
+
(async () => {
|
|
24
|
+
console.log('=== Lathe Cylinder Radial Test (0.025mm resolution) ===');
|
|
25
|
+
|
|
26
|
+
// Load STL
|
|
27
|
+
const stlPath = '${path.join(__dirname, '../../benchmark/fixtures/lathe-cylinder.stl')}';
|
|
28
|
+
const stlBuffer = await (await fetch('file://' + stlPath)).arrayBuffer();
|
|
29
|
+
|
|
30
|
+
console.log('STL Size:', (stlBuffer.byteLength / 1024 / 1024).toFixed(2), 'MB');
|
|
31
|
+
|
|
32
|
+
// Parse STL
|
|
33
|
+
function parseSTL(arrayBuffer) {
|
|
34
|
+
const view = new DataView(arrayBuffer);
|
|
35
|
+
const triangleCount = view.getUint32(80, true);
|
|
36
|
+
console.log('Triangle count:', triangleCount);
|
|
37
|
+
|
|
38
|
+
const triangles = new Float32Array(triangleCount * 9);
|
|
39
|
+
let offset = 84;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < triangleCount; i++) {
|
|
42
|
+
offset += 12; // Skip normal
|
|
43
|
+
for (let j = 0; j < 9; j++) {
|
|
44
|
+
triangles[i * 9 + j] = view.getFloat32(offset, true);
|
|
45
|
+
offset += 4;
|
|
46
|
+
}
|
|
47
|
+
offset += 2; // Skip attribute byte count
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return triangles;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const triangles = parseSTL(stlBuffer);
|
|
54
|
+
|
|
55
|
+
// Calculate bounds
|
|
56
|
+
let minX = Infinity, maxX = -Infinity;
|
|
57
|
+
let minY = Infinity, maxY = -Infinity;
|
|
58
|
+
let minZ = Infinity, maxZ = -Infinity;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
61
|
+
const x = triangles[i];
|
|
62
|
+
const y = triangles[i + 1];
|
|
63
|
+
const z = triangles[i + 2];
|
|
64
|
+
minX = Math.min(minX, x); maxX = Math.max(maxX, x);
|
|
65
|
+
minY = Math.min(minY, y); maxY = Math.max(maxY, y);
|
|
66
|
+
minZ = Math.min(minZ, z); maxZ = Math.max(maxZ, z);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('Bounds:');
|
|
70
|
+
console.log(' X:', minX.toFixed(2), 'to', maxX.toFixed(2), '(size:', (maxX - minX).toFixed(2), 'mm)');
|
|
71
|
+
console.log(' Y:', minY.toFixed(2), 'to', maxY.toFixed(2), '(size:', (maxY - minY).toFixed(2), 'mm)');
|
|
72
|
+
console.log(' Z:', minZ.toFixed(2), 'to', maxZ.toFixed(2), '(size:', (maxZ - minZ).toFixed(2), 'mm)');
|
|
73
|
+
|
|
74
|
+
const resolution = 0.025;
|
|
75
|
+
const rotationStep = 1.0;
|
|
76
|
+
|
|
77
|
+
console.log('\\nTest parameters:');
|
|
78
|
+
console.log(' Resolution:', resolution, 'mm');
|
|
79
|
+
console.log(' Rotation step:', rotationStep, 'degrees');
|
|
80
|
+
|
|
81
|
+
// Calculate expected dimensions
|
|
82
|
+
const xSize = maxX - minX;
|
|
83
|
+
const ySize = maxY - minY;
|
|
84
|
+
const zSize = maxZ - minZ;
|
|
85
|
+
const maxRadius = Math.sqrt(ySize * ySize + zSize * zSize) / 2;
|
|
86
|
+
|
|
87
|
+
const gridXSize = Math.ceil(xSize / resolution);
|
|
88
|
+
const gridYHeight = Math.ceil(ySize / resolution);
|
|
89
|
+
const numAngles = Math.ceil(360 / rotationStep);
|
|
90
|
+
|
|
91
|
+
console.log('\\nExpected grid dimensions:');
|
|
92
|
+
console.log(' X grid size:', gridXSize, 'cells');
|
|
93
|
+
console.log(' Y grid height:', gridYHeight, 'cells');
|
|
94
|
+
console.log(' Number of angles:', numAngles);
|
|
95
|
+
console.log(' Max radius:', maxRadius.toFixed(2), 'mm');
|
|
96
|
+
|
|
97
|
+
// Calculate memory requirements
|
|
98
|
+
const bytesPerCell = 4; // f32
|
|
99
|
+
const totalCells = gridXSize * gridYHeight * numAngles;
|
|
100
|
+
const totalMemoryMB = (totalCells * bytesPerCell / 1024 / 1024).toFixed(2);
|
|
101
|
+
|
|
102
|
+
console.log('\\nMemory requirements:');
|
|
103
|
+
console.log(' Total cells:', totalCells.toLocaleString());
|
|
104
|
+
console.log(' Total memory:', totalMemoryMB, 'MB');
|
|
105
|
+
|
|
106
|
+
// Test with RasterPath
|
|
107
|
+
console.log('\\n=== Starting RasterPath test ===');
|
|
108
|
+
|
|
109
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
110
|
+
const raster = new RasterPath({
|
|
111
|
+
mode: 'radial',
|
|
112
|
+
resolution: resolution,
|
|
113
|
+
rotationStep: rotationStep
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await raster.init();
|
|
117
|
+
console.log('RasterPath initialized');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Load terrain
|
|
121
|
+
console.log('Loading terrain...');
|
|
122
|
+
const startLoad = Date.now();
|
|
123
|
+
await raster.loadTerrain({ triangles, zFloor: 0 });
|
|
124
|
+
const loadTime = Date.now() - startLoad;
|
|
125
|
+
console.log('Terrain loaded in', loadTime, 'ms');
|
|
126
|
+
|
|
127
|
+
// Generate toolpaths with a dummy tool
|
|
128
|
+
console.log('Generating toolpaths...');
|
|
129
|
+
const startToolpath = Date.now();
|
|
130
|
+
|
|
131
|
+
// Create a simple cylindrical tool (10mm diameter, 10mm height)
|
|
132
|
+
const toolRadius = 5;
|
|
133
|
+
const toolHeight = 10;
|
|
134
|
+
const toolSegments = 16;
|
|
135
|
+
const toolTriangles = new Float32Array(toolSegments * 2 * 9);
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < toolSegments; i++) {
|
|
138
|
+
const angle1 = (i / toolSegments) * Math.PI * 2;
|
|
139
|
+
const angle2 = ((i + 1) / toolSegments) * Math.PI * 2;
|
|
140
|
+
|
|
141
|
+
const x1 = Math.cos(angle1) * toolRadius;
|
|
142
|
+
const y1 = Math.sin(angle1) * toolRadius;
|
|
143
|
+
const x2 = Math.cos(angle2) * toolRadius;
|
|
144
|
+
const y2 = Math.sin(angle2) * toolRadius;
|
|
145
|
+
|
|
146
|
+
// Bottom triangle
|
|
147
|
+
const idx = i * 18;
|
|
148
|
+
toolTriangles[idx] = 0; toolTriangles[idx + 1] = 0; toolTriangles[idx + 2] = 0;
|
|
149
|
+
toolTriangles[idx + 3] = x1; toolTriangles[idx + 4] = y1; toolTriangles[idx + 5] = 0;
|
|
150
|
+
toolTriangles[idx + 6] = x2; toolTriangles[idx + 7] = y2; toolTriangles[idx + 8] = 0;
|
|
151
|
+
|
|
152
|
+
// Side rectangle (as 2 triangles) - just one for simplicity
|
|
153
|
+
toolTriangles[idx + 9] = x1; toolTriangles[idx + 10] = y1; toolTriangles[idx + 11] = 0;
|
|
154
|
+
toolTriangles[idx + 12] = x1; toolTriangles[idx + 13] = y1; toolTriangles[idx + 14] = toolHeight;
|
|
155
|
+
toolTriangles[idx + 15] = x2; toolTriangles[idx + 16] = y2; toolTriangles[idx + 17] = 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await raster.loadTool({ triangles: toolTriangles });
|
|
159
|
+
|
|
160
|
+
const toolpathData = await raster.generateToolpaths({
|
|
161
|
+
xStep: 10,
|
|
162
|
+
yStep: 10,
|
|
163
|
+
zFloor: 0,
|
|
164
|
+
radiusOffset: 20
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const toolpathTime = Date.now() - startToolpath;
|
|
168
|
+
|
|
169
|
+
console.log('\\n=== Test Results ===');
|
|
170
|
+
console.log('Toolpath generation time:', toolpathTime, 'ms');
|
|
171
|
+
console.log('Number of strips:', toolpathData.numStrips);
|
|
172
|
+
console.log('Total points:', toolpathData.totalPoints);
|
|
173
|
+
console.log('Average points per strip:', (toolpathData.totalPoints / toolpathData.numStrips).toFixed(0));
|
|
174
|
+
console.log('\\nTEST PASSED');
|
|
175
|
+
|
|
176
|
+
return { success: true };
|
|
177
|
+
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('\\n=== TEST FAILED ===');
|
|
180
|
+
console.error('Error:', error.message);
|
|
181
|
+
console.error('Stack:', error.stack);
|
|
182
|
+
return { success: false, error: error.message };
|
|
183
|
+
} finally {
|
|
184
|
+
raster.terminate();
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
`).then((result) => {
|
|
188
|
+
testPassed = result.success === true;
|
|
189
|
+
app.quit();
|
|
190
|
+
}).catch((error) => {
|
|
191
|
+
console.error('Test execution error:', error);
|
|
192
|
+
app.quit();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
app.on('window-all-closed', () => {
|
|
197
|
+
process.exit(testPassed ? 0 : 1);
|
|
198
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// radial-thread-limit-test.cjs
|
|
2
|
+
// Test that radial rasterization respects thread limits and produces correct output
|
|
3
|
+
|
|
4
|
+
const { app, BrowserWindow } = require('electron');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
let mainWindow;
|
|
9
|
+
|
|
10
|
+
function createWindow() {
|
|
11
|
+
mainWindow = new BrowserWindow({
|
|
12
|
+
width: 1200,
|
|
13
|
+
height: 800,
|
|
14
|
+
show: false,
|
|
15
|
+
webPreferences: {
|
|
16
|
+
nodeIntegration: false,
|
|
17
|
+
contextIsolation: true,
|
|
18
|
+
enableBlinkFeatures: 'WebGPU',
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
23
|
+
mainWindow.loadFile(htmlPath);
|
|
24
|
+
|
|
25
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
26
|
+
console.log('✓ Page loaded');
|
|
27
|
+
|
|
28
|
+
const testScript = `
|
|
29
|
+
(async function() {
|
|
30
|
+
console.log('=== Radial Thread Limit Test ===\\n');
|
|
31
|
+
|
|
32
|
+
if (!navigator.gpu) {
|
|
33
|
+
return { error: 'WebGPU not available' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
37
|
+
|
|
38
|
+
// Read test STL
|
|
39
|
+
const stlPath = '${path.join(__dirname, '../../benchmark/fixtures/lathe-cylinder.stl')}';
|
|
40
|
+
const response = await fetch('file://' + stlPath);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
return { error: 'Failed to load test STL: ' + stlPath };
|
|
43
|
+
}
|
|
44
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
45
|
+
|
|
46
|
+
// Test with different thread limits
|
|
47
|
+
const threadLimits = [
|
|
48
|
+
{ limit: 256, desc: 'Default limit (256 threads)' },
|
|
49
|
+
{ limit: 128, desc: 'Reduced limit (128 threads)' },
|
|
50
|
+
{ limit: 64, desc: 'Very low limit (64 threads)' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const config of threadLimits) {
|
|
56
|
+
console.log(\`Testing: \${config.desc}\`);
|
|
57
|
+
|
|
58
|
+
const raster = new RasterPath({
|
|
59
|
+
mode: 'radial',
|
|
60
|
+
resolution: 0.5,
|
|
61
|
+
rotationStep: 1.0,
|
|
62
|
+
toolWidth: 5.0,
|
|
63
|
+
maxConcurrentThreads: config.limit
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await raster.init();
|
|
67
|
+
|
|
68
|
+
const startTime = performance.now();
|
|
69
|
+
const result = await raster.processSTL(arrayBuffer);
|
|
70
|
+
const elapsed = performance.now() - startTime;
|
|
71
|
+
|
|
72
|
+
// Calculate checksum
|
|
73
|
+
let checksum = 0;
|
|
74
|
+
for (const strip of result.strips) {
|
|
75
|
+
for (let i = 0; i < strip.pathData.length; i++) {
|
|
76
|
+
checksum = (checksum * 31 + strip.pathData[i]) | 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(\` Time: \${elapsed.toFixed(1)}ms\`);
|
|
81
|
+
console.log(\` Strips: \${result.strips.length}\`);
|
|
82
|
+
console.log(\` Checksum: \${checksum}\`);
|
|
83
|
+
console.log('');
|
|
84
|
+
|
|
85
|
+
results.push({
|
|
86
|
+
limit: config.limit,
|
|
87
|
+
desc: config.desc,
|
|
88
|
+
time: elapsed,
|
|
89
|
+
strips: result.strips.length,
|
|
90
|
+
checksum,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Verify all checksums match
|
|
95
|
+
const referenceChecksum = results[0].checksum;
|
|
96
|
+
const allMatch = results.every(r => r.checksum === referenceChecksum);
|
|
97
|
+
|
|
98
|
+
console.log('=== Results ===');
|
|
99
|
+
console.log(\`Reference checksum: \${referenceChecksum}\`);
|
|
100
|
+
console.log(\`All checksums match: \${allMatch ? '✓ YES' : '❌ NO'}\`);
|
|
101
|
+
|
|
102
|
+
if (!allMatch) {
|
|
103
|
+
console.log('\\nChecksum mismatches:');
|
|
104
|
+
for (const r of results) {
|
|
105
|
+
if (r.checksum !== referenceChecksum) {
|
|
106
|
+
console.log(\` \${r.desc}: \${r.checksum} (expected \${referenceChecksum})\`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('\\nTiming comparison:');
|
|
112
|
+
const baselineTime = results[0].time;
|
|
113
|
+
for (const r of results) {
|
|
114
|
+
const ratio = (r.time / baselineTime).toFixed(2);
|
|
115
|
+
console.log(\` \${r.desc}: \${r.time.toFixed(1)}ms (\${ratio}x)\`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { success: allMatch, results };
|
|
119
|
+
})();
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
124
|
+
|
|
125
|
+
if (result.error) {
|
|
126
|
+
console.error('❌ Test failed:', result.error);
|
|
127
|
+
app.exit(1);
|
|
128
|
+
} else if (!result.success) {
|
|
129
|
+
console.error('❌ Thread limit test failed - checksums do not match');
|
|
130
|
+
app.exit(1);
|
|
131
|
+
} else {
|
|
132
|
+
console.log('\\n✅ Thread limit test passed - all configurations produce identical output');
|
|
133
|
+
app.exit(0);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('❌ Test error:', error);
|
|
137
|
+
app.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
142
|
+
console.log(message);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
app.whenReady().then(createWindow);
|
|
147
|
+
|
|
148
|
+
app.on('window-all-closed', () => {
|
|
149
|
+
if (process.platform !== 'darwin') {
|
|
150
|
+
app.quit();
|
|
151
|
+
}
|
|
152
|
+
});
|