@gridspace/raster-path 1.0.7 → 1.0.9

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,164 @@
1
+ // Radial V3 batched bucket rasterization
2
+ // Processes ALL buckets in one dispatch - GPU threads find their bucket
3
+
4
+ const EPSILON: f32 = 0.0001;
5
+
6
+ struct Uniforms {
7
+ resolution: f32, // Grid step size (mm)
8
+ tool_radius: f32, // Tool radius for Y-filtering
9
+ full_grid_width: u32, // Full grid width (all buckets)
10
+ grid_height: u32, // Number of Y cells
11
+ global_min_x: f32, // Global minimum X coordinate
12
+ bucket_min_y: f32, // Y-axis start (typically -tool_width/2)
13
+ z_floor: f32, // Z value for empty cells
14
+ num_buckets: u32, // Number of buckets
15
+ }
16
+
17
+ struct BucketInfo {
18
+ min_x: f32, // Bucket X range start
19
+ max_x: f32, // Bucket X range end
20
+ start_index: u32, // Index into triangle_indices array
21
+ count: u32, // Number of triangles in this bucket
22
+ }
23
+
24
+ @group(0) @binding(0) var<storage, read> rotated_triangles: array<f32>; // ALL rotated triangles + bounds
25
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>; // Full-width output grid
26
+ @group(0) @binding(2) var<uniform> uniforms: Uniforms;
27
+ @group(0) @binding(3) var<storage, read> all_buckets: array<BucketInfo>; // All bucket descriptors
28
+ @group(0) @binding(4) var<storage, read> triangle_indices: array<u32>; // All triangle indices
29
+
30
+ // Simplified ray-triangle intersection for downward rays
31
+ fn ray_triangle_intersect_downward(
32
+ ray_origin: vec3<f32>,
33
+ v0: vec3<f32>,
34
+ v1: vec3<f32>,
35
+ v2: vec3<f32>
36
+ ) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, t: distance along ray)
37
+ let ray_dir = vec3<f32>(0.0, 0.0, -1.0);
38
+
39
+ let edge1 = v1 - v0;
40
+ let edge2 = v2 - v0;
41
+ let h = cross(ray_dir, edge2);
42
+ let a = dot(edge1, h);
43
+
44
+ if (a > -EPSILON && a < EPSILON) {
45
+ return vec2<f32>(0.0, 0.0);
46
+ }
47
+
48
+ let f = 1.0 / a;
49
+ let s = ray_origin - v0;
50
+ let u = f * dot(s, h);
51
+
52
+ if (u < -EPSILON || u > 1.0 + EPSILON) {
53
+ return vec2<f32>(0.0, 0.0);
54
+ }
55
+
56
+ let q = cross(s, edge1);
57
+ let v = f * dot(ray_dir, q);
58
+
59
+ if (v < -EPSILON || u + v > 1.0 + EPSILON) {
60
+ return vec2<f32>(0.0, 0.0);
61
+ }
62
+
63
+ let t = f * dot(edge2, q);
64
+
65
+ if (t > EPSILON) {
66
+ return vec2<f32>(1.0, t);
67
+ }
68
+
69
+ return vec2<f32>(0.0, 0.0);
70
+ }
71
+
72
+ @compute @workgroup_size(8, 8, 1)
73
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
74
+ let grid_x = global_id.x;
75
+ let grid_y = global_id.y;
76
+
77
+ // Bounds check
78
+ if (grid_x >= uniforms.full_grid_width || grid_y >= uniforms.grid_height) {
79
+ return;
80
+ }
81
+
82
+ // Calculate world position
83
+ let world_x = uniforms.global_min_x + f32(grid_x) * uniforms.resolution;
84
+ let world_y = uniforms.bucket_min_y + f32(grid_y) * uniforms.resolution;
85
+
86
+ // FIND WHICH BUCKET THIS X POSITION BELONGS TO
87
+ // Simple linear search (could be binary search for many buckets)
88
+ var bucket_idx = 0u;
89
+ var found_bucket = false;
90
+ for (var i = 0u; i < uniforms.num_buckets; i++) {
91
+ if (world_x >= all_buckets[i].min_x && world_x < all_buckets[i].max_x) {
92
+ bucket_idx = i;
93
+ found_bucket = true;
94
+ break;
95
+ }
96
+ }
97
+
98
+ // If not in any bucket, write floor and return
99
+ if (!found_bucket) {
100
+ let output_idx = grid_y * uniforms.full_grid_width + grid_x;
101
+ output[output_idx] = uniforms.z_floor;
102
+ return;
103
+ }
104
+
105
+ let bucket = all_buckets[bucket_idx];
106
+
107
+ // Fixed downward ray from high above
108
+ let ray_origin = vec3<f32>(world_x, world_y, 1000.0);
109
+
110
+ // Track best (closest) hit
111
+ var best_z = uniforms.z_floor;
112
+
113
+ // Test triangles in this bucket with Y-bounds filtering
114
+ for (var i = 0u; i < bucket.count; i++) {
115
+ // Get triangle index from bucket's index array
116
+ let tri_idx = triangle_indices[bucket.start_index + i];
117
+
118
+ // Read Y-bounds first (cheaper than reading all vertices)
119
+ let base = tri_idx * 11u;
120
+ let y_min = rotated_triangles[base + 9u];
121
+ let y_max = rotated_triangles[base + 10u];
122
+
123
+ // Y-bounds check: skip triangles that don't overlap this ray's Y position
124
+ if (y_max < world_y - uniforms.tool_radius ||
125
+ y_min > world_y + uniforms.tool_radius) {
126
+ continue;
127
+ }
128
+
129
+ // Read rotated vertices
130
+ let v0 = vec3<f32>(
131
+ rotated_triangles[base],
132
+ rotated_triangles[base + 1u],
133
+ rotated_triangles[base + 2u]
134
+ );
135
+ let v1 = vec3<f32>(
136
+ rotated_triangles[base + 3u],
137
+ rotated_triangles[base + 4u],
138
+ rotated_triangles[base + 5u]
139
+ );
140
+ let v2 = vec3<f32>(
141
+ rotated_triangles[base + 6u],
142
+ rotated_triangles[base + 7u],
143
+ rotated_triangles[base + 8u]
144
+ );
145
+
146
+ let result = ray_triangle_intersect_downward(ray_origin, v0, v1, v2);
147
+ let hit = result.x;
148
+ let t = result.y;
149
+
150
+ if (hit > 0.5) {
151
+ // Calculate Z position of intersection
152
+ let hit_z = ray_origin.z - t;
153
+
154
+ // Keep highest (max Z) hit
155
+ if (hit_z > best_z) {
156
+ best_z = hit_z;
157
+ }
158
+ }
159
+ }
160
+
161
+ // Write to FULL-WIDTH output (no stitching needed!)
162
+ let output_idx = grid_y * uniforms.full_grid_width + grid_x;
163
+ output[output_idx] = best_z;
164
+ }
@@ -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">