@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.
- package/README.md +144 -8
- package/build/app.js +36 -0
- package/build/index.html +3 -0
- package/build/raster-path.js +10 -2
- package/build/raster-worker.js +657 -75
- package/package.json +3 -2
- package/src/core/path-radial-v3.js +405 -0
- package/src/core/path-tracing.js +206 -115
- package/src/core/raster-config.js +24 -0
- package/src/core/raster-path.js +10 -2
- package/src/core/raster-worker.js +10 -0
- package/src/shaders/radial-rasterize-batched.wgsl +164 -0
- package/src/shaders/radial-rotate-triangles.wgsl +70 -0
- package/src/test/radial-v3-benchmark.cjs +184 -0
- package/src/test/radial-v3-bucket-test.cjs +154 -0
- package/src/web/app.js +36 -0
- package/src/web/index.html +3 -0
|
@@ -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);
|
package/src/web/index.html
CHANGED
|
@@ -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">
|