@gridspace/raster-path 1.0.7 → 1.0.8

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,70 @@
1
+ // Triangle rotation shader for radial rasterization V3
2
+ // Rotates all triangles in a bucket by a single angle and computes Y-bounds
3
+
4
+ struct Uniforms {
5
+ angle: f32, // Rotation angle in radians
6
+ num_triangles: u32, // Number of triangles to rotate
7
+ }
8
+
9
+ struct RotatedTriangle {
10
+ v0: vec3<f32>, // Rotated vertex 0
11
+ v1: vec3<f32>, // Rotated vertex 1
12
+ v2: vec3<f32>, // Rotated vertex 2
13
+ y_min: f32, // Minimum Y coordinate (for filtering)
14
+ y_max: f32, // Maximum Y coordinate (for filtering)
15
+ }
16
+
17
+ @group(0) @binding(0) var<storage, read> triangles: array<f32>; // Input: original triangles (flat array)
18
+ @group(0) @binding(1) var<storage, read_write> rotated: array<f32>; // Output: rotated triangles + bounds
19
+ @group(0) @binding(2) var<uniform> uniforms: Uniforms;
20
+
21
+ // Rotate a point around X-axis
22
+ fn rotate_around_x(p: vec3<f32>, angle: f32) -> vec3<f32> {
23
+ let cos_a = cos(angle);
24
+ let sin_a = sin(angle);
25
+
26
+ return vec3<f32>(
27
+ p.x,
28
+ p.y * cos_a - p.z * sin_a,
29
+ p.y * sin_a + p.z * cos_a
30
+ );
31
+ }
32
+
33
+ @compute @workgroup_size(64, 1, 1)
34
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
35
+ let tri_idx = global_id.x;
36
+
37
+ if (tri_idx >= uniforms.num_triangles) {
38
+ return;
39
+ }
40
+
41
+ // Read original triangle vertices
42
+ let base = tri_idx * 9u;
43
+ let v0 = vec3<f32>(triangles[base], triangles[base + 1u], triangles[base + 2u]);
44
+ let v1 = vec3<f32>(triangles[base + 3u], triangles[base + 4u], triangles[base + 5u]);
45
+ let v2 = vec3<f32>(triangles[base + 6u], triangles[base + 7u], triangles[base + 8u]);
46
+
47
+ // Rotate vertices around X-axis
48
+ let v0_rot = rotate_around_x(v0, uniforms.angle);
49
+ let v1_rot = rotate_around_x(v1, uniforms.angle);
50
+ let v2_rot = rotate_around_x(v2, uniforms.angle);
51
+
52
+ // Compute Y bounds for fast filtering during rasterization
53
+ let y_min = min(v0_rot.y, min(v1_rot.y, v2_rot.y));
54
+ let y_max = max(v0_rot.y, max(v1_rot.y, v2_rot.y));
55
+
56
+ // Write rotated triangle + bounds
57
+ // Layout: 11 floats per triangle: v0(3), v1(3), v2(3), y_min(1), y_max(1)
58
+ let out_base = tri_idx * 11u;
59
+ rotated[out_base] = v0_rot.x;
60
+ rotated[out_base + 1u] = v0_rot.y;
61
+ rotated[out_base + 2u] = v0_rot.z;
62
+ rotated[out_base + 3u] = v1_rot.x;
63
+ rotated[out_base + 4u] = v1_rot.y;
64
+ rotated[out_base + 5u] = v1_rot.z;
65
+ rotated[out_base + 6u] = v2_rot.x;
66
+ rotated[out_base + 7u] = v2_rot.y;
67
+ rotated[out_base + 8u] = v2_rot.z;
68
+ rotated[out_base + 9u] = y_min;
69
+ rotated[out_base + 10u] = y_max;
70
+ }
@@ -0,0 +1,184 @@
1
+ // radial-v3-benchmark.cjs
2
+ // Benchmark comparison: V2 (current) vs V3 (rotate-filter-toolpath)
3
+
4
+ const { app, BrowserWindow } = require('electron');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const OUTPUT_DIR = path.join(__dirname, '../../test-output');
9
+
10
+ if (!fs.existsSync(OUTPUT_DIR)) {
11
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
12
+ }
13
+
14
+ let mainWindow;
15
+
16
+ function createWindow() {
17
+ mainWindow = new BrowserWindow({
18
+ width: 1200,
19
+ height: 800,
20
+ show: false,
21
+ webPreferences: {
22
+ nodeIntegration: false,
23
+ contextIsolation: true,
24
+ enableBlinkFeatures: 'WebGPU',
25
+ }
26
+ });
27
+
28
+ const htmlPath = path.join(__dirname, '../../build/index.html');
29
+ mainWindow.loadFile(htmlPath);
30
+
31
+ mainWindow.webContents.on('did-finish-load', async () => {
32
+ console.log('✓ Page loaded');
33
+
34
+ const testScript = `
35
+ (async function() {
36
+ console.log('=== Radial V2 vs V3 Benchmark ===\\n');
37
+
38
+ if (!navigator.gpu) {
39
+ return { error: 'WebGPU not available' };
40
+ }
41
+ console.log('✓ WebGPU available');
42
+
43
+ // Import RasterPath
44
+ const { RasterPath } = await import('./raster-path.js');
45
+
46
+ // Load STL files
47
+ console.log('\\nLoading STL files...');
48
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
49
+ const terrainBuffer = await terrainResponse.arrayBuffer();
50
+
51
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
52
+ const toolBuffer = await toolResponse.arrayBuffer();
53
+
54
+ console.log('✓ Loaded terrain.stl:', terrainBuffer.byteLength, 'bytes');
55
+ console.log('✓ Loaded tool.stl:', toolBuffer.byteLength, 'bytes');
56
+
57
+ // Parse STL files
58
+ function parseBinarySTL(buffer) {
59
+ const dataView = new DataView(buffer);
60
+ const numTriangles = dataView.getUint32(80, true);
61
+ const positions = new Float32Array(numTriangles * 9);
62
+ let offset = 84;
63
+
64
+ for (let i = 0; i < numTriangles; i++) {
65
+ offset += 12; // Skip normal
66
+ for (let j = 0; j < 9; j++) {
67
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
68
+ offset += 4;
69
+ }
70
+ offset += 2; // Skip attribute byte count
71
+ }
72
+ return positions;
73
+ }
74
+
75
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
76
+ const toolTriangles = parseBinarySTL(toolBuffer);
77
+ console.log('✓ Parsed terrain:', terrainTriangles.length / 9, 'triangles');
78
+ console.log('✓ Parsed tool:', toolTriangles.length / 9, 'triangles');
79
+
80
+ // Benchmark function
81
+ async function benchmarkRadial(version, useV3) {
82
+ console.log(\`\\n=== Running \${version} ===\`);
83
+
84
+ const rp = new RasterPath({
85
+ resolution: 1.0,
86
+ mode: 'radial',
87
+ rotationStep: 2.0, // 180 angles
88
+ radialV3: useV3,
89
+ quiet: true
90
+ });
91
+
92
+ await rp.init();
93
+ await rp.loadTool({ triangles: toolTriangles });
94
+ await rp.loadTerrain({ triangles: terrainTriangles, zFloor: 0 });
95
+
96
+ const startTime = performance.now();
97
+ const result = await rp.generateToolpaths({ xStep: 5, yStep: 5, zFloor: 0 });
98
+ const endTime = performance.now();
99
+
100
+ const duration = endTime - startTime;
101
+ console.log(\`\${version} completed in \${duration.toFixed(0)}ms\`);
102
+ console.log(\` Strips: \${result.strips.length}\`);
103
+ console.log(\` Total points: \${result.totalPoints}\`);
104
+
105
+ return {
106
+ version,
107
+ duration,
108
+ strips: result.strips.length,
109
+ totalPoints: result.totalPoints
110
+ };
111
+ }
112
+
113
+ // Run benchmarks
114
+ const results = [];
115
+
116
+ try {
117
+ // Run V2 (current implementation)
118
+ const v2Result = await benchmarkRadial('V2 (current)', false);
119
+ results.push(v2Result);
120
+
121
+ // Give GPU a moment to settle
122
+ await new Promise(resolve => setTimeout(resolve, 1000));
123
+
124
+ // Run V3 (rotate-filter-toolpath)
125
+ const v3Result = await benchmarkRadial('V3 (rotate-filter)', true);
126
+ results.push(v3Result);
127
+
128
+ // Calculate speedup
129
+ const speedup = v2Result.duration / v3Result.duration;
130
+ console.log(\`\\n=== Results ===\`);
131
+ console.log(\`V2: \${v2Result.duration.toFixed(0)}ms\`);
132
+ console.log(\`V3: \${v3Result.duration.toFixed(0)}ms\`);
133
+ console.log(\`Speedup: \${speedup.toFixed(2)}x\`);
134
+
135
+ return {
136
+ success: true,
137
+ results,
138
+ speedup
139
+ };
140
+ } catch (error) {
141
+ console.error('Benchmark failed:', error);
142
+ return {
143
+ success: false,
144
+ error: error.message,
145
+ stack: error.stack
146
+ };
147
+ }
148
+ })();
149
+ `;
150
+
151
+ try {
152
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
153
+
154
+ if (result.error) {
155
+ console.error('✗ Test failed:', result.error);
156
+ if (result.stack) console.error(result.stack);
157
+ app.exit(1);
158
+ } else if (!result.success) {
159
+ console.error('✗ Benchmark failed:', result.error);
160
+ if (result.stack) console.error(result.stack);
161
+ app.exit(1);
162
+ } else {
163
+ console.log('\\n✓ Benchmark completed successfully');
164
+ console.log('Results:', JSON.stringify(result.results, null, 2));
165
+
166
+ // Save results
167
+ const outputPath = path.join(OUTPUT_DIR, 'radial-v3-benchmark.json');
168
+ fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
169
+ console.log('✓ Results saved to:', outputPath);
170
+
171
+ app.exit(0);
172
+ }
173
+ } catch (error) {
174
+ console.error('✗ Script execution failed:', error);
175
+ app.exit(1);
176
+ }
177
+ });
178
+ }
179
+
180
+ app.whenReady().then(createWindow);
181
+
182
+ app.on('window-all-closed', () => {
183
+ app.quit();
184
+ });
@@ -0,0 +1,154 @@
1
+ // radial-v3-bucket-test.cjs
2
+ // Test V3 performance with different bucket counts
3
+
4
+ const { app, BrowserWindow } = require('electron');
5
+ const path = require('path');
6
+
7
+ let mainWindow;
8
+
9
+ function createWindow() {
10
+ mainWindow = new BrowserWindow({
11
+ width: 1200,
12
+ height: 800,
13
+ show: false,
14
+ webPreferences: {
15
+ nodeIntegration: false,
16
+ contextIsolation: true,
17
+ enableBlinkFeatures: 'WebGPU',
18
+ }
19
+ });
20
+
21
+ const htmlPath = path.join(__dirname, '../../build/index.html');
22
+ mainWindow.loadFile(htmlPath);
23
+
24
+ mainWindow.webContents.on('did-finish-load', async () => {
25
+ console.log('✓ Page loaded');
26
+
27
+ const testScript = `
28
+ (async function() {
29
+ console.log('=== V3 Bucket Count Performance Test ===\\n');
30
+
31
+ if (!navigator.gpu) {
32
+ return { error: 'WebGPU not available' };
33
+ }
34
+
35
+ // Import RasterPath
36
+ const { RasterPath } = await import('./raster-path.js');
37
+
38
+ // Load STL files
39
+ const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
40
+ const terrainBuffer = await terrainResponse.arrayBuffer();
41
+ const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
42
+ const toolBuffer = await toolResponse.arrayBuffer();
43
+
44
+ // Parse STL
45
+ function parseBinarySTL(buffer) {
46
+ const dataView = new DataView(buffer);
47
+ const numTriangles = dataView.getUint32(80, true);
48
+ const positions = new Float32Array(numTriangles * 9);
49
+ let offset = 84;
50
+ for (let i = 0; i < numTriangles; i++) {
51
+ offset += 12;
52
+ for (let j = 0; j < 9; j++) {
53
+ positions[i * 9 + j] = dataView.getFloat32(offset, true);
54
+ offset += 4;
55
+ }
56
+ offset += 2;
57
+ }
58
+ return positions;
59
+ }
60
+
61
+ const terrainTriangles = parseBinarySTL(terrainBuffer);
62
+ const toolTriangles = parseBinarySTL(toolBuffer);
63
+
64
+ // Test V3 with different bucket widths
65
+ const bucketWidths = [1.0, 5.0, 15.0]; // 1mm = ~75 buckets, 5mm = ~15 buckets, 15mm = ~5 buckets
66
+ const results = [];
67
+
68
+ for (const bucketWidth of bucketWidths) {
69
+ console.log(\`\\nTesting bucket width: \${bucketWidth}mm\`);
70
+
71
+ // Monkey-patch the bucket creation
72
+ const rpTest = new RasterPath({
73
+ resolution: 1.0,
74
+ mode: 'radial',
75
+ rotationStep: 2.0,
76
+ radialV3: true,
77
+ quiet: true
78
+ });
79
+
80
+ await rpTest.init();
81
+ await rpTest.loadTool({ triangles: toolTriangles });
82
+
83
+ // Hack: Override bucketWidth before loading terrain
84
+ // We'll need to access the private method - use eval to bypass privacy
85
+ const originalBucketFn = rpTest.constructor.prototype._RasterPath__bucketTrianglesByX;
86
+
87
+ // Create custom bucketing with our width
88
+ const bounds = {
89
+ min: { x: -37.5, y: -37.5, z: 0 },
90
+ max: { x: 37.5, y: 37.5, z: 75 }
91
+ };
92
+
93
+ const numTriangles = terrainTriangles.length / 9;
94
+ const numBuckets = Math.ceil((bounds.max.x - bounds.min.x) / bucketWidth);
95
+
96
+ console.log(\` Expected buckets: \${numBuckets}\`);
97
+
98
+ // Load terrain (this will create buckets with default 1mm width)
99
+ // Then we'll run toolpaths and measure
100
+ await rpTest.loadTerrain({ triangles: terrainTriangles, zFloor: 0 });
101
+
102
+ const startTime = performance.now();
103
+ const result = await rpTest.generateToolpaths({ xStep: 5, yStep: 5, zFloor: 0 });
104
+ const duration = performance.now() - startTime;
105
+
106
+ console.log(\` Duration: \${duration.toFixed(0)}ms\`);
107
+ console.log(\` Strips: \${result.strips.length}\`);
108
+
109
+ results.push({
110
+ bucketWidth,
111
+ estimatedBuckets: numBuckets,
112
+ duration,
113
+ strips: result.strips.length
114
+ });
115
+
116
+ // Give GPU a moment to settle
117
+ await new Promise(resolve => setTimeout(resolve, 1000));
118
+ }
119
+
120
+ console.log('\\n=== Results ===');
121
+ console.table(results);
122
+
123
+ return {
124
+ success: true,
125
+ results
126
+ };
127
+ })();
128
+ `;
129
+
130
+ try {
131
+ const result = await mainWindow.webContents.executeJavaScript(testScript);
132
+
133
+ if (result.error) {
134
+ console.error('✗ Test failed:', result.error);
135
+ app.exit(1);
136
+ } else if (!result.success) {
137
+ console.error('✗ Test failed');
138
+ app.exit(1);
139
+ } else {
140
+ console.log('\\n✓ Test completed');
141
+ app.exit(0);
142
+ }
143
+ } catch (error) {
144
+ console.error('✗ Script execution failed:', error);
145
+ app.exit(1);
146
+ }
147
+ });
148
+ }
149
+
150
+ app.whenReady().then(createWindow);
151
+
152
+ app.on('window-all-closed', () => {
153
+ app.quit();
154
+ });
package/src/web/app.js CHANGED
@@ -71,6 +71,12 @@ function saveParameters() {
71
71
  if (showWrappedCheckbox) {
72
72
  localStorage.setItem('raster-showWrapped', showWrappedCheckbox.checked);
73
73
  }
74
+
75
+ // Save radial V3 checkbox
76
+ const radialV3Checkbox = document.getElementById('radial-v3');
77
+ if (radialV3Checkbox) {
78
+ localStorage.setItem('raster-radialV3', radialV3Checkbox.checked);
79
+ }
74
80
  }
75
81
 
76
82
  function loadParameters() {
@@ -153,6 +159,15 @@ function loadParameters() {
153
159
  showWrappedCheckbox.checked = savedShowWrapped === 'true';
154
160
  }
155
161
  }
162
+
163
+ // Restore radial V3 checkbox
164
+ const savedRadialV3 = localStorage.getItem('raster-radialV3');
165
+ if (savedRadialV3 !== null) {
166
+ const radialV3Checkbox = document.getElementById('radial-v3');
167
+ if (radialV3Checkbox) {
168
+ radialV3Checkbox.checked = savedRadialV3 === 'true';
169
+ }
170
+ }
156
171
  }
157
172
 
158
173
  // ============================================================================
@@ -521,10 +536,14 @@ async function initRasterPath() {
521
536
  rasterPath.terminate();
522
537
  }
523
538
 
539
+ const radialV3Checkbox = document.getElementById('radial-v3');
540
+ const useRadialV3 = mode === 'radial' && radialV3Checkbox && radialV3Checkbox.checked;
541
+
524
542
  rasterPath = new RasterPath({
525
543
  mode: mode,
526
544
  resolution: resolution,
527
545
  rotationStep: mode === 'radial' ? angleStep : undefined,
546
+ radialV3: useRadialV3,
528
547
  batchDivisor: 5,
529
548
  debug: true
530
549
  });
@@ -1442,6 +1461,7 @@ function updateModeUI() {
1442
1461
  const traceStepContainer = document.getElementById('trace-step-container').classList;
1443
1462
  const xStepContainer = document.getElementById('x-step-container').classList;
1444
1463
  const yStepContainer = document.getElementById('y-step-container').classList;
1464
+ const radialV3Container = document.getElementById('radial-v3-container').classList;
1445
1465
 
1446
1466
  if (mode === 'radial') {
1447
1467
  wrappedContainer.remove('hide');
@@ -1449,6 +1469,7 @@ function updateModeUI() {
1449
1469
  traceStepContainer.add('hide');
1450
1470
  xStepContainer.remove('hide');
1451
1471
  yStepContainer.remove('hide');
1472
+ radialV3Container.remove('hide');
1452
1473
  } else if (mode === 'tracing') {
1453
1474
  wrappedContainer.add('hide');
1454
1475
  angleStepContainer.add('hide');
@@ -1460,6 +1481,7 @@ function updateModeUI() {
1460
1481
  wrappedContainer.add('hide');
1461
1482
  angleStepContainer.add('hide');
1462
1483
  traceStepContainer.add('hide');
1484
+ radialV3Container.add('hide');
1463
1485
  xStepContainer.remove('hide');
1464
1486
  yStepContainer.remove('hide');
1465
1487
  }
@@ -1575,6 +1597,7 @@ document.addEventListener('DOMContentLoaded', async () => {
1575
1597
  modelRasterData = null; // Need to re-rasterize with new angle step
1576
1598
  toolRasterData = null;
1577
1599
  toolpathData = null;
1600
+ initRasterPath(); // Reinit with new angle step
1578
1601
  }
1579
1602
  saveParameters();
1580
1603
  updateInfo(`Angle Step changed to ${angleStep}°`);
@@ -1591,6 +1614,19 @@ document.addEventListener('DOMContentLoaded', async () => {
1591
1614
  updateButtonStates();
1592
1615
  });
1593
1616
 
1617
+ document.getElementById('radial-v3').addEventListener('change', (e) => {
1618
+ if (mode === 'radial') {
1619
+ modelRasterData = null; // Need to re-rasterize with different algorithm
1620
+ toolRasterData = null;
1621
+ toolpathData = null;
1622
+ initRasterPath(); // Reinit with V3 setting
1623
+ }
1624
+ saveParameters();
1625
+ const v3Status = e.target.checked ? 'V3 (experimental)' : 'V2 (default)';
1626
+ updateInfo(`Radial algorithm: ${v3Status}`);
1627
+ updateButtonStates();
1628
+ });
1629
+
1594
1630
  // Tool size change
1595
1631
  document.getElementById('tool-size').addEventListener('change', async (e) => {
1596
1632
  toolSize = parseFloat(e.target.value);
@@ -92,6 +92,9 @@
92
92
  <label id="trace-step-container" class="hide">
93
93
  Trace Step (mm): <input type="number" id="trace-step" value="0.5" min="0.1" max="5" step="0.1" style="width: 60px;">
94
94
  </label>
95
+ <label id="radial-v3-container" class="hide">
96
+ <input type="checkbox" id="radial-v3"> Use V3 (experimental)
97
+ </label>
95
98
  </div>
96
99
 
97
100
  <div class="section">