@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gridspace/raster-path",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "description": "Terrain and Tool Raster Path Finder using WebGPU",
6
6
  "type": "module",
@@ -36,7 +36,11 @@
36
36
  "test": "npm run test:planar && npm run test:radial",
37
37
  "test:planar": "npm run build && npx electron src/test/planar-test.cjs",
38
38
  "test:planar-tiling": "npm run build && npx electron src/test/planar-tiling-test.cjs",
39
- "test:radial": "npm run build && npx electron src/test/radial-test.cjs"
39
+ "test:radial": "npm run build && npx electron src/test/radial-test.cjs",
40
+ "test:batch-divisor": "npm run build && npx electron src/test/batch-divisor-benchmark.cjs",
41
+ "test:work-estimation": "npm run build && npx electron src/test/work-estimation-profile.cjs",
42
+ "test:workload-calibration": "npm run build && npx electron src/test/workload-calibration.cjs",
43
+ "test:lathe-cylinder-2-debug": "npm run build && npx electron src/test/lathe-cylinder-2-debug.cjs"
40
44
  },
41
45
  "keywords": [
42
46
  "cnc",
@@ -21,7 +21,7 @@ const WORKER_DEST = path.join(BUILD_DIR, 'webgpu-worker.js');
21
21
  let workerCode = fs.readFileSync(WORKER_SRC, 'utf8');
22
22
 
23
23
  // Find all shader placeholders
24
- const shaderRegex = /\/\*SHADER:([a-z0-9-]+)\*\//g;
24
+ const shaderRegex = /'SHADER:([a-z0-9-]+)'/g;
25
25
  let match;
26
26
  const replacements = [];
27
27
 
package/src/index.js CHANGED
@@ -55,6 +55,7 @@
55
55
  * @property {number} resolution - Grid step size in mm (required)
56
56
  * @property {number} rotationStep - Radial mode only: degrees between rays (e.g., 1.0 = 360 rays)
57
57
  * @property {number} trianglesPerTile - Target triangles per tile for radial rasterization (default: calculated)
58
+ * @property {number} batchDivisor - Testing parameter to artificially divide batch size (default: 1)
58
59
  * @property {boolean} debug - Enable debug logging (default: false)
59
60
  * @property {boolean} quiet - Suppress log output (default: false)
60
61
  */
@@ -103,18 +104,13 @@ export class RasterPath {
103
104
  this.deviceCapabilities = null;
104
105
 
105
106
  // Configure debug output
106
- let urlOpt = [];
107
107
  if (config.quiet) {
108
108
  debug.log = function() {};
109
- urlOpt.push('quiet');
110
- }
111
- if (config.debug) {
112
- urlOpt.push('debug');
113
109
  }
114
110
 
115
111
  // Configuration with defaults
116
112
  this.config = {
117
- workerName: (config.workerName ?? "webgpu-worker.js") + (urlOpt.length ? "?"+urlOpt.join('&') : ""),
113
+ workerName: config.workerName ?? "webgpu-worker.js",
118
114
  maxGPUMemoryMB: config.maxGPUMemoryMB ?? 256,
119
115
  gpuMemorySafetyMargin: config.gpuMemorySafetyMargin ?? 0.8,
120
116
  autoTiling: config.autoTiling ?? true,
@@ -122,7 +118,12 @@ export class RasterPath {
122
118
  maxConcurrentTiles: config.maxConcurrentTiles ?? 10,
123
119
  trianglesPerTile: config.trianglesPerTile, // undefined = auto-calculate
124
120
  radialRotationOffset: config.radialRotationOffset ?? 0, // degrees
121
+ batchDivisor: config.batchDivisor ?? 1, // For testing batching overhead
122
+ debug: config.debug,
123
+ quiet: config.quiet
125
124
  };
125
+
126
+ debug.log('config', this.config);
126
127
  }
127
128
 
128
129
  /**
@@ -238,8 +239,8 @@ export class RasterPath {
238
239
  const originalBounds = boundsOverride || this.#calculateBounds(triangles);
239
240
 
240
241
  // Center model in YZ plane (required for radial rasterization)
241
- // Radial mode casts rays from origin, so terrain must be centered at (0,0) in YZ
242
- // to ensure rays intersect the geometry symmetrically around the rotation axis
242
+ // Radial mode casts rays from max_radius distance inward toward the X-axis,
243
+ // and centering ensures the geometry is symmetric around the rotation axis
243
244
  const centerY = (originalBounds.min.y + originalBounds.max.y) / 2;
244
245
  const centerZ = (originalBounds.min.z + originalBounds.max.z) / 2;
245
246
 
@@ -273,12 +274,10 @@ export class RasterPath {
273
274
  * @param {number} params.xStep - Sample every Nth point in X direction
274
275
  * @param {number} params.yStep - Sample every Nth point in Y direction
275
276
  * @param {number} params.zFloor - Z floor value for out-of-bounds areas
276
- * @param {number} params.radiusOffset - (Radial mode only) Distance from terrain surface to tool tip in mm.
277
- * Used to calculate radial collision offset. Default: 20mm
278
277
  * @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
279
278
  * @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
280
279
  */
281
- async generateToolpaths({ xStep, yStep, zFloor, radiusOffset = 20, onProgress }) {
280
+ async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
282
281
  if (!this.isInitialized) {
283
282
  throw new Error('RasterPath not initialized. Call init() first.');
284
283
  }
@@ -287,6 +286,8 @@ export class RasterPath {
287
286
  throw new Error('Tool not loaded. Call loadTool() first.');
288
287
  }
289
288
 
289
+ debug.log('gen.paths', { xStep, yStep, zFloor });
290
+
290
291
  if (this.mode === 'planar') {
291
292
  if (!this.terrainData) {
292
293
  throw new Error('Terrain not loaded. Call loadTerrain() first.');
@@ -444,8 +445,7 @@ export class RasterPath {
444
445
  zFloor: zFloor,
445
446
  bounds,
446
447
  xStep,
447
- yStep,
448
- gridStep: this.resolution
448
+ yStep
449
449
  },
450
450
  'radial-toolpaths-complete',
451
451
  completionHandler
@@ -17,6 +17,7 @@ struct Uniforms {
17
17
  filter_mode: u32, // 0 = max Z (terrain), 1 = min Z (tool)
18
18
  num_buckets: u32, // Total number of X-buckets
19
19
  start_angle: f32, // Starting angle offset in radians (for batching)
20
+ bucket_offset: u32, // Offset for bucket batching (bucket_idx in batch writes to bucket_offset + bucket_idx in output)
20
21
  }
21
22
 
22
23
  struct BucketInfo {
@@ -125,6 +126,10 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
125
126
 
126
127
  // Step 2: Rotate position (scan_x, scan_y, scan_z) around X-axis by 'angle'
127
128
  // X stays the same, rotate YZ plane: y' = y*cos - z*sin, z' = y*sin + z*cos
129
+ // NOTE: This uses right-handed rotation (positive angle rotates +Y towards +Z)
130
+ // To reverse rotation direction (left-handed or opposite), flip signs:
131
+ // y' = y*cos + z*sin (flip sign on z term)
132
+ // z' = -y*sin + z*cos (flip sign on y term)
128
133
  let ray_origin_x = scan_x;
129
134
  let ray_origin_y = scan_y * cos(angle) - scan_z * sin(angle);
130
135
  let ray_origin_z = scan_y * sin(angle) + scan_z * cos(angle);
@@ -132,6 +137,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
132
137
 
133
138
  // Step 3: Rotate ray direction (0, 0, -1) around X-axis by 'angle'
134
139
  // X component stays 0, rotate YZ: dy = 0*cos - (-1)*sin = sin, dz = 0*sin + (-1)*cos = -cos
140
+ // NOTE: For reversed rotation, use: vec3<f32>(0.0, -sin(angle), -cos(angle))
135
141
  let ray_dir = vec3<f32>(0.0, sin(angle), -cos(angle));
136
142
 
137
143
  // Initialize best distance (closest hit)
@@ -174,11 +180,11 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
174
180
  }
175
181
 
176
182
  // Write output
177
- // Layout: bucket_idx * numAngles * bucketWidth * gridHeight
183
+ // Layout: (bucket_offset + bucket_idx) * numAngles * bucketWidth * gridHeight
178
184
  // + angle_idx * bucketWidth * gridHeight
179
185
  // + grid_y * bucketWidth
180
186
  // + local_x
181
- let output_idx = bucket_idx * uniforms.num_angles * uniforms.bucket_grid_width * uniforms.grid_y_height
187
+ let output_idx = (uniforms.bucket_offset + bucket_idx) * uniforms.num_angles * uniforms.bucket_grid_width * uniforms.grid_y_height
182
188
  + angle_idx * uniforms.bucket_grid_width * uniforms.grid_y_height
183
189
  + grid_y * uniforms.bucket_grid_width
184
190
  + local_x;
@@ -0,0 +1,286 @@
1
+ // batch-divisor-benchmark.cjs
2
+ // Benchmark test to measure batching overhead with different batch divisors
3
+ // Usage: node batch-divisor-benchmark.cjs [divisor1,divisor2,...]
4
+ // Example: node batch-divisor-benchmark.cjs 1,2,4,8,16,32
5
+
6
+ const { app, BrowserWindow } = require('electron');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ const OUTPUT_DIR = path.join(__dirname, '../../test-output');
11
+ const RESULTS_FILE = path.join(OUTPUT_DIR, 'batch-divisor-results.json');
12
+
13
+ if (!fs.existsSync(OUTPUT_DIR)) {
14
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
15
+ }
16
+
17
+ // Parse batch divisors from command line args or use defaults
18
+ const args = process.argv.slice(2);
19
+ const BATCH_DIVISORS = args.length > 0
20
+ ? args[0].split(',').map(n => parseInt(n.trim()))
21
+ : [1, 2, 4, 8, 16, 32];
22
+
23
+ console.log('=== Batch Divisor Benchmark ===');
24
+ console.log('Testing with divisors:', BATCH_DIVISORS.join(', '));
25
+ console.log('');
26
+
27
+ let mainWindow;
28
+ let currentDivisorIndex = 0;
29
+ const results = [];
30
+
31
+ function createWindow() {
32
+ mainWindow = new BrowserWindow({
33
+ width: 1200,
34
+ height: 800,
35
+ show: false,
36
+ webPreferences: {
37
+ nodeIntegration: false,
38
+ contextIsolation: true,
39
+ enableBlinkFeatures: 'WebGPU',
40
+ }
41
+ });
42
+
43
+ const htmlPath = path.join(__dirname, '../../build/index.html');
44
+ mainWindow.loadFile(htmlPath);
45
+
46
+ mainWindow.webContents.on('did-finish-load', async () => {
47
+ console.log('✓ Page loaded');
48
+ await runNextTest();
49
+ });
50
+
51
+ // Capture console output from renderer process
52
+ mainWindow.webContents.on('console-message', (event, level, message) => {
53
+ // Filter for our timing logs
54
+ if (message.includes('Batch') && message.includes('timing:')) {
55
+ console.log('[TIMING]', message);
56
+ }
57
+ });
58
+ }
59
+
60
+ async function runNextTest() {
61
+ if (currentDivisorIndex >= BATCH_DIVISORS.length) {
62
+ // All tests complete - analyze and report
63
+ await analyzeResults();
64
+ return;
65
+ }
66
+
67
+ const divisor = BATCH_DIVISORS[currentDivisorIndex];
68
+ console.log(`\n${'='.repeat(60)}`);
69
+ console.log(`Testing with BATCH_DIVISOR = ${divisor}`);
70
+ console.log('='.repeat(60));
71
+
72
+ const testScript = `
73
+ (async function() {
74
+ const divisor = ${divisor};
75
+
76
+ if (!navigator.gpu) {
77
+ return { error: 'WebGPU not available' };
78
+ }
79
+
80
+ // Import RasterPath
81
+ const { RasterPath } = await import('./raster-path.js');
82
+
83
+ // Load STL files (same as radial-test.cjs - large enough to require batching)
84
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
85
+ const terrainBuffer = await terrainResponse.arrayBuffer();
86
+
87
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
88
+ const toolBuffer = await toolResponse.arrayBuffer();
89
+
90
+ // Parse STL files
91
+ function parseBinarySTL(buffer) {
92
+ const dataView = new DataView(buffer);
93
+ const numTriangles = dataView.getUint32(80, true);
94
+ const positions = new Float32Array(numTriangles * 9);
95
+ let offset = 84;
96
+
97
+ for (let i = 0; i < numTriangles; i++) {
98
+ offset += 12; // Skip normal
99
+ for (let j = 0; j < 9; j++) {
100
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
101
+ offset += 4;
102
+ }
103
+ offset += 2; // Skip attribute byte count
104
+ }
105
+ return positions;
106
+ }
107
+
108
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
109
+ const toolTriangles = parseBinarySTL(toolBuffer);
110
+
111
+ // Test parameters - configured to require batching
112
+ const resolution = 0.05; // 0.05mm - finer resolution to force batching
113
+ const rotationStep = 1.0; // 1 degree between rays = 360 angles
114
+ const xStep = 1;
115
+ const yStep = 1;
116
+ const zFloor = 0;
117
+ const radiusOffset = 20;
118
+
119
+ console.log('Test parameters:');
120
+ console.log(' Resolution:', resolution, 'mm');
121
+ console.log(' Rotation step:', rotationStep, '°');
122
+ console.log(' Batch divisor:', divisor);
123
+
124
+ // Create RasterPath instance with specific batch divisor
125
+ const raster = new RasterPath({
126
+ mode: 'radial',
127
+ resolution: resolution,
128
+ rotationStep: rotationStep,
129
+ batchDivisor: divisor
130
+ });
131
+ await raster.init();
132
+
133
+ // Load tool
134
+ const t0 = performance.now();
135
+ await raster.loadTool({ triangles: toolTriangles });
136
+ const toolTime = performance.now() - t0;
137
+
138
+ // Load terrain
139
+ const t1 = performance.now();
140
+ await raster.loadTerrain({ triangles: terrainTriangles, zFloor: zFloor });
141
+ const terrainTime = performance.now() - t1;
142
+
143
+ // Generate toolpaths (this is where batching happens)
144
+ const t2 = performance.now();
145
+ const toolpathData = await raster.generateToolpaths({
146
+ xStep: xStep,
147
+ yStep: yStep,
148
+ zFloor: zFloor,
149
+ radiusOffset: radiusOffset
150
+ });
151
+ const toolpathTime = performance.now() - t2;
152
+
153
+ // Cleanup
154
+ raster.terminate();
155
+
156
+ return {
157
+ success: true,
158
+ divisor: divisor,
159
+ timing: {
160
+ tool: toolTime,
161
+ terrain: terrainTime,
162
+ toolpath: toolpathTime,
163
+ total: toolTime + terrainTime + toolpathTime
164
+ },
165
+ result: {
166
+ numStrips: toolpathData.numStrips,
167
+ totalPoints: toolpathData.totalPoints
168
+ }
169
+ };
170
+ })();
171
+ `;
172
+
173
+ try {
174
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
175
+
176
+ if (result.error) {
177
+ console.error('❌ Test failed:', result.error);
178
+ app.exit(1);
179
+ return;
180
+ }
181
+
182
+ results.push(result);
183
+
184
+ console.log(`\nResults for BATCH_DIVISOR = ${divisor}:`);
185
+ console.log(` Tool load time: ${result.timing.tool.toFixed(1)}ms`);
186
+ console.log(` Terrain load time: ${result.timing.terrain.toFixed(1)}ms`);
187
+ console.log(` Toolpath time: ${result.timing.toolpath.toFixed(1)}ms`);
188
+ console.log(` Total time: ${result.timing.total.toFixed(1)}ms`);
189
+ console.log(` Strips generated: ${result.result.numStrips}`);
190
+ console.log(` Total points: ${result.result.totalPoints}`);
191
+
192
+ // Move to next test
193
+ currentDivisorIndex++;
194
+
195
+ // Small delay before next test to ensure clean state
196
+ setTimeout(() => runNextTest(), 1000);
197
+
198
+ } catch (error) {
199
+ console.error('Error running test:', error);
200
+ app.exit(1);
201
+ }
202
+ }
203
+
204
+ async function analyzeResults() {
205
+ console.log('\n' + '='.repeat(60));
206
+ console.log('ANALYSIS');
207
+ console.log('='.repeat(60));
208
+
209
+ // Save raw results
210
+ const resultsData = {
211
+ timestamp: new Date().toISOString(),
212
+ divisors: BATCH_DIVISORS,
213
+ results: results
214
+ };
215
+ fs.writeFileSync(RESULTS_FILE, JSON.stringify(resultsData, null, 2));
216
+ console.log(`\n✓ Raw results saved to: ${RESULTS_FILE}`);
217
+
218
+ // Analyze overhead
219
+ console.log('\n--- Timing Comparison ---');
220
+ const baseline = results[0]; // Divisor = 1
221
+ console.log(`\nBaseline (divisor=1): ${baseline.timing.total.toFixed(1)}ms total`);
222
+ console.log(` Breakdown: ${baseline.timing.tool.toFixed(1)}ms tool + ${baseline.timing.terrain.toFixed(1)}ms terrain + ${baseline.timing.toolpath.toFixed(1)}ms toolpath`);
223
+
224
+ console.log('\nOverhead Analysis:');
225
+ console.log('┌──────────┬───────────┬────────────┬──────────────┬──────────────┐');
226
+ console.log('│ Divisor │ Total (ms)│ vs Baseline│ Overhead (ms)│ Overhead (%) │');
227
+ console.log('├──────────┼───────────┼────────────┼──────────────┼──────────────┤');
228
+
229
+ for (const result of results) {
230
+ const overhead = result.timing.total - baseline.timing.total;
231
+ const overheadPercent = ((overhead / baseline.timing.total) * 100);
232
+ const comparison = result.divisor === 1 ? 'baseline' : `+${overhead.toFixed(0)}ms`;
233
+
234
+ console.log(
235
+ `│ ${String(result.divisor).padEnd(8)} │ ` +
236
+ `${result.timing.total.toFixed(1).padStart(9)} │ ` +
237
+ `${comparison.padStart(10)} │ ` +
238
+ `${overhead.toFixed(1).padStart(12)} │ ` +
239
+ `${overheadPercent.toFixed(1).padStart(12)}% │`
240
+ );
241
+ }
242
+ console.log('└──────────┴───────────┴────────────┴──────────────┴──────────────┘');
243
+
244
+ // Calculate per-batch overhead
245
+ if (results.length > 1) {
246
+ console.log('\n--- Per-Batch Overhead Estimation ---');
247
+ // Assume divisor creates divisor times more batches
248
+ // So divisor=2 creates 2x batches, divisor=4 creates 4x batches, etc.
249
+ for (let i = 1; i < results.length; i++) {
250
+ const result = results[i];
251
+ const extraBatches = result.divisor - 1; // Assuming baseline has 1 effective batch unit
252
+ const overhead = result.timing.total - baseline.timing.total;
253
+ const perBatchOverhead = overhead / (result.divisor - 1);
254
+
255
+ console.log(`Divisor ${result.divisor}: ${overhead.toFixed(1)}ms overhead / ${extraBatches} extra batch(es) ≈ ${perBatchOverhead.toFixed(1)}ms per batch boundary`);
256
+ }
257
+ }
258
+
259
+ // Recommendations
260
+ console.log('\n--- Recommendations ---');
261
+ const maxResult = results[results.length - 1];
262
+ const maxOverheadPercent = ((maxResult.timing.total - baseline.timing.total) / baseline.timing.total) * 100;
263
+
264
+ if (maxOverheadPercent < 15) {
265
+ console.log('✓ LOW OVERHEAD (<15%): Batching overhead is acceptable.');
266
+ console.log(' Focus on other optimizations (shader efficiency, toolpath generation).');
267
+ } else if (maxOverheadPercent < 30) {
268
+ console.log('⚠ MEDIUM OVERHEAD (15-30%): Consider batch size tuning.');
269
+ console.log(' Investigate buffer creation/destruction costs.');
270
+ console.log(' Consider reusing buffers across batches.');
271
+ } else {
272
+ console.log('⚠ HIGH OVERHEAD (>30%): Priority optimization needed!');
273
+ console.log(' Critical to reduce batch overhead before increasing batch count.');
274
+ console.log(' Primary suspects:');
275
+ console.log(' - createReusableToolpathBuffers() per batch');
276
+ console.log(' - destroyReusableToolpathBuffers() per batch');
277
+ console.log(' - GPU context switching between batches');
278
+ console.log(' Recommendation: Implement buffer pooling or batch-level buffer reuse.');
279
+ }
280
+
281
+ console.log('\n✅ Benchmark complete!');
282
+ app.exit(0);
283
+ }
284
+
285
+ app.whenReady().then(createWindow);
286
+ app.on('window-all-closed', () => app.quit());