@gridspace/raster-path 1.0.2

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,213 @@
1
+ // Planar rasterization with spatial partitioning
2
+ // Sentinel value for empty cells (far below any real geometry)
3
+ const EMPTY_CELL: f32 = -1e10;
4
+
5
+ struct Uniforms {
6
+ bounds_min_x: f32,
7
+ bounds_min_y: f32,
8
+ bounds_min_z: f32,
9
+ bounds_max_x: f32,
10
+ bounds_max_y: f32,
11
+ bounds_max_z: f32,
12
+ step_size: f32,
13
+ grid_width: u32,
14
+ grid_height: u32,
15
+ triangle_count: u32,
16
+ filter_mode: u32, // 0 = UPWARD (terrain, keep highest), 1 = DOWNWARD (tool, keep lowest)
17
+ spatial_grid_width: u32,
18
+ spatial_grid_height: u32,
19
+ spatial_cell_size: f32,
20
+ }
21
+
22
+ @group(0) @binding(0) var<storage, read> triangles: array<f32>;
23
+ @group(0) @binding(1) var<storage, read_write> output_points: array<f32>;
24
+ @group(0) @binding(2) var<storage, read_write> valid_mask: array<u32>;
25
+ @group(0) @binding(3) var<uniform> uniforms: Uniforms;
26
+ @group(0) @binding(4) var<storage, read> spatial_cell_offsets: array<u32>;
27
+ @group(0) @binding(5) var<storage, read> spatial_triangle_indices: array<u32>;
28
+
29
+ // Fast 2D bounding box check for XY plane
30
+ fn ray_hits_triangle_bbox_2d(ray_x: f32, ray_y: f32, v0: vec3<f32>, v1: vec3<f32>, v2: vec3<f32>) -> bool {
31
+ // Add small epsilon to catch near-misses (mesh vertex gaps, FP rounding)
32
+ let epsilon = 0.001; // 1 micron tolerance
33
+ let min_x = min(min(v0.x, v1.x), v2.x) - epsilon;
34
+ let max_x = max(max(v0.x, v1.x), v2.x) + epsilon;
35
+ let min_y = min(min(v0.y, v1.y), v2.y) - epsilon;
36
+ let max_y = max(max(v0.y, v1.y), v2.y) + epsilon;
37
+
38
+ return ray_x >= min_x && ray_x <= max_x && ray_y >= min_y && ray_y <= max_y;
39
+ }
40
+
41
+ // Ray-triangle intersection using Möller-Trumbore algorithm
42
+ fn ray_triangle_intersect(
43
+ ray_origin: vec3<f32>,
44
+ ray_dir: vec3<f32>,
45
+ v0: vec3<f32>,
46
+ v1: vec3<f32>,
47
+ v2: vec3<f32>
48
+ ) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, z: intersection_z)
49
+ // Larger epsilon needed because near-parallel triangles (small 'a') amplify errors via f=1/a
50
+ let EPSILON = 0.0001;
51
+
52
+ // Early rejection using 2D bounding box (very cheap!)
53
+ if (!ray_hits_triangle_bbox_2d(ray_origin.x, ray_origin.y, v0, v1, v2)) {
54
+ return vec2<f32>(0.0, 0.0);
55
+ }
56
+
57
+ // Calculate edges
58
+ let edge1 = v1 - v0;
59
+ let edge2 = v2 - v0;
60
+
61
+ // Cross product: ray_dir × edge2
62
+ let h = cross(ray_dir, edge2);
63
+
64
+ // Dot product: edge1 · h
65
+ let a = dot(edge1, h);
66
+
67
+ if (a > -EPSILON && a < EPSILON) {
68
+ return vec2<f32>(0.0, 0.0); // Ray parallel to triangle
69
+ }
70
+
71
+ let f = 1.0 / a;
72
+
73
+ // s = ray_origin - v0
74
+ let s = ray_origin - v0;
75
+
76
+ // u = f * (s · h)
77
+ let u = f * dot(s, h);
78
+
79
+ // Allow tolerance for edges/vertices to ensure watertight coverage
80
+ if (u < -EPSILON || u > 1.0 + EPSILON) {
81
+ return vec2<f32>(0.0, 0.0);
82
+ }
83
+
84
+ // Cross product: s × edge1
85
+ let q = cross(s, edge1);
86
+
87
+ // v = f * (ray_dir · q)
88
+ let v = f * dot(ray_dir, q);
89
+
90
+ // Allow tolerance for edges/vertices to ensure watertight coverage
91
+ if (v < -EPSILON || u + v > 1.0 + EPSILON) {
92
+ return vec2<f32>(0.0, 0.0);
93
+ }
94
+
95
+ // t = f * (edge2 · q)
96
+ let t = f * dot(edge2, q);
97
+
98
+ if (t > EPSILON) {
99
+ // Intersection found - calculate Z coordinate
100
+ let intersection_z = ray_origin.z + ray_dir.z * t;
101
+ return vec2<f32>(1.0, intersection_z);
102
+ }
103
+
104
+ return vec2<f32>(0.0, 0.0);
105
+ }
106
+
107
+ @compute @workgroup_size(16, 16)
108
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
109
+ let grid_x = global_id.x;
110
+ let grid_y = global_id.y;
111
+
112
+ if (grid_x >= uniforms.grid_width || grid_y >= uniforms.grid_height) {
113
+ return;
114
+ }
115
+
116
+ // Calculate world position for this grid point (center of cell)
117
+ let world_x = uniforms.bounds_min_x + f32(grid_x) * uniforms.step_size;
118
+ let world_y = uniforms.bounds_min_y + f32(grid_y) * uniforms.step_size;
119
+
120
+ // Initialize best_z based on filter mode
121
+ var best_z: f32;
122
+ if (uniforms.filter_mode == 0u) {
123
+ best_z = -1e10; // Terrain: keep highest Z
124
+ } else {
125
+ best_z = 1e10; // Tool: keep lowest Z
126
+ }
127
+
128
+ var found = false;
129
+
130
+ // Ray from below mesh pointing up (+Z direction)
131
+ let ray_origin = vec3<f32>(world_x, world_y, uniforms.bounds_min_z - 1.0);
132
+ let ray_dir = vec3<f32>(0.0, 0.0, 1.0);
133
+
134
+ // Find which spatial grid cell this ray belongs to
135
+ let spatial_cell_x = u32((world_x - uniforms.bounds_min_x) / uniforms.spatial_cell_size);
136
+ let spatial_cell_y = u32((world_y - uniforms.bounds_min_y) / uniforms.spatial_cell_size);
137
+
138
+ // Clamp to spatial grid bounds
139
+ let clamped_cx = min(spatial_cell_x, uniforms.spatial_grid_width - 1u);
140
+ let clamped_cy = min(spatial_cell_y, uniforms.spatial_grid_height - 1u);
141
+
142
+ let spatial_cell_idx = clamped_cy * uniforms.spatial_grid_width + clamped_cx;
143
+
144
+ // Get triangle range for this cell
145
+ let start_idx = spatial_cell_offsets[spatial_cell_idx];
146
+ let end_idx = spatial_cell_offsets[spatial_cell_idx + 1u];
147
+
148
+ // Test only triangles in this spatial cell
149
+ for (var idx = start_idx; idx < end_idx; idx++) {
150
+ let tri_idx = spatial_triangle_indices[idx];
151
+ let tri_base = tri_idx * 9u;
152
+
153
+ // Read triangle vertices (already in local space and rotated if needed)
154
+ let v0 = vec3<f32>(
155
+ triangles[tri_base],
156
+ triangles[tri_base + 1u],
157
+ triangles[tri_base + 2u]
158
+ );
159
+ let v1 = vec3<f32>(
160
+ triangles[tri_base + 3u],
161
+ triangles[tri_base + 4u],
162
+ triangles[tri_base + 5u]
163
+ );
164
+ let v2 = vec3<f32>(
165
+ triangles[tri_base + 6u],
166
+ triangles[tri_base + 7u],
167
+ triangles[tri_base + 8u]
168
+ );
169
+
170
+ let result = ray_triangle_intersect(ray_origin, ray_dir, v0, v1, v2);
171
+ let hit = result.x;
172
+ let intersection_z = result.y;
173
+
174
+ if (hit > 0.5) {
175
+ if (uniforms.filter_mode == 0u) {
176
+ // Terrain: keep highest
177
+ if (intersection_z > best_z) {
178
+ best_z = intersection_z;
179
+ found = true;
180
+ }
181
+ } else {
182
+ // Tool: keep lowest
183
+ if (intersection_z < best_z) {
184
+ best_z = intersection_z;
185
+ found = true;
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ // Write output based on filter mode
192
+ let output_idx = grid_y * uniforms.grid_width + grid_x;
193
+
194
+ if (uniforms.filter_mode == 0u) {
195
+ // Terrain: Dense output (Z-only, sentinel value for empty cells)
196
+ if (found) {
197
+ output_points[output_idx] = best_z;
198
+ } else {
199
+ output_points[output_idx] = EMPTY_CELL;
200
+ }
201
+ } else {
202
+ // Tool: Sparse output (X, Y, Z triplets with valid mask)
203
+ output_points[output_idx * 3u] = f32(grid_x);
204
+ output_points[output_idx * 3u + 1u] = f32(grid_y);
205
+ output_points[output_idx * 3u + 2u] = best_z;
206
+
207
+ if (found) {
208
+ valid_mask[output_idx] = 1u;
209
+ } else {
210
+ valid_mask[output_idx] = 0u;
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,83 @@
1
+ // Planar toolpath generation
2
+ // Sentinel value for empty terrain cells (must match rasterize shader)
3
+ const EMPTY_CELL: f32 = -1e10;
4
+ const MAX_F32: f32 = 3.402823466e+38;
5
+
6
+ struct SparseToolPoint {
7
+ x_offset: i32,
8
+ y_offset: i32,
9
+ z_value: f32,
10
+ padding: f32,
11
+ }
12
+
13
+ struct Uniforms {
14
+ terrain_width: u32,
15
+ terrain_height: u32,
16
+ tool_count: u32,
17
+ x_step: u32,
18
+ y_step: u32,
19
+ oob_z: f32,
20
+ points_per_line: u32,
21
+ num_scanlines: u32,
22
+ y_offset: u32, // Offset to center Y position (for single-scanline radial mode)
23
+ }
24
+
25
+ @group(0) @binding(0) var<storage, read> terrain_map: array<f32>;
26
+ @group(0) @binding(1) var<storage, read> sparse_tool: array<SparseToolPoint>;
27
+ @group(0) @binding(2) var<storage, read_write> output_path: array<f32>;
28
+ @group(0) @binding(3) var<uniform> uniforms: Uniforms;
29
+
30
+ @compute @workgroup_size(16, 16)
31
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
32
+ let scanline = global_id.y;
33
+ let point_idx = global_id.x;
34
+
35
+ if (scanline >= uniforms.num_scanlines || point_idx >= uniforms.points_per_line) {
36
+ return;
37
+ }
38
+
39
+ let tool_center_x = i32(point_idx * uniforms.x_step);
40
+ let tool_center_y = i32(scanline * uniforms.y_step) + i32(uniforms.y_offset);
41
+
42
+ // var max_collision_z = -MAX_F32; // Track maximum collision height
43
+ var max_collision_z = uniforms.oob_z; // Track maximum collision height
44
+ var found_collision = false;
45
+
46
+ for (var i = 0u; i < uniforms.tool_count; i++) {
47
+ let tool_point = sparse_tool[i];
48
+ let terrain_x = tool_center_x + tool_point.x_offset;
49
+ let terrain_y = tool_center_y + tool_point.y_offset;
50
+
51
+ // Bounds check: X must always be in bounds
52
+ if (terrain_x < 0 || terrain_x >= i32(uniforms.terrain_width)) {
53
+ continue;
54
+ }
55
+
56
+ // Bounds check: Y must be within terrain strip bounds
57
+ // For single-scanline OUTPUT mode, the tool center is at Y=0 but the terrain
58
+ // strip contains the full Y range (tool width), so tool offsets access different Y rows
59
+ if (terrain_y < 0 || terrain_y >= i32(uniforms.terrain_height)) {
60
+ continue;
61
+ }
62
+
63
+ let terrain_idx = u32(terrain_y) * uniforms.terrain_width + u32(terrain_x);
64
+ let terrain_z = terrain_map[terrain_idx];
65
+
66
+ // Check if terrain cell has geometry (not empty sentinel value)
67
+ if (terrain_z > EMPTY_CELL + 1.0) {
68
+ // Tool z_value is positive offset from tip (tip=0, shaft=+50)
69
+ // Subtract from terrain to find where tool center needs to be
70
+ let collision_z = terrain_z + tool_point.z_value;
71
+ max_collision_z = max(max_collision_z, collision_z);
72
+ found_collision = true;
73
+ }
74
+ }
75
+
76
+ var output_z = uniforms.oob_z;
77
+ if (found_collision) {
78
+ output_z = max_collision_z;
79
+ }
80
+
81
+ let output_idx = scanline * uniforms.points_per_line + point_idx;
82
+ output_path[output_idx] = output_z;
83
+ }
@@ -0,0 +1,195 @@
1
+ // Radial V2 rasterization with X-bucketing and rotating ray planes
2
+ // Sentinel value for empty cells (far below any real geometry)
3
+ const EMPTY_CELL: f32 = -1e10;
4
+ const PI: f32 = 3.14159265359;
5
+
6
+ struct Uniforms {
7
+ resolution: f32, // Grid step size (mm)
8
+ angle_step: f32, // Radians between angles
9
+ num_angles: u32, // Total number of angular strips
10
+ max_radius: f32, // Ray origin distance from X-axis (maxHypot * 1.01)
11
+ tool_width: f32, // Tool width in mm
12
+ grid_y_height: u32, // Tool width in pixels (toolWidth / resolution)
13
+ bucket_width: f32, // Width of each X-bucket (mm)
14
+ bucket_grid_width: u32, // Bucket width in pixels
15
+ global_min_x: f32, // Global minimum X coordinate
16
+ z_floor: f32, // Z value for empty cells
17
+ filter_mode: u32, // 0 = max Z (terrain), 1 = min Z (tool)
18
+ num_buckets: u32, // Total number of X-buckets
19
+ start_angle: f32, // Starting angle offset in radians (for batching)
20
+ }
21
+
22
+ struct BucketInfo {
23
+ min_x: f32, // Bucket X range start (mm)
24
+ max_x: f32, // Bucket X range end (mm)
25
+ start_index: u32, // Index into triangle_indices array
26
+ count: u32 // Number of triangles in this bucket
27
+ }
28
+
29
+ @group(0) @binding(0) var<storage, read> triangles: array<f32>;
30
+ @group(0) @binding(1) var<storage, read_write> output: array<f32>;
31
+ @group(0) @binding(2) var<uniform> uniforms: Uniforms;
32
+ @group(0) @binding(3) var<storage, read> bucket_info: array<BucketInfo>;
33
+ @group(0) @binding(4) var<storage, read> triangle_indices: array<u32>;
34
+
35
+ // Note: AABB early rejection removed - X-bucketing already provides spatial filtering
36
+ // A proper ray-AABB intersection test would be needed if we wanted bounding box culling,
37
+ // but checking if ray_origin is inside AABB was incorrect and rejected valid triangles
38
+
39
+ // Ray-triangle intersection using Möller-Trumbore algorithm
40
+ fn ray_triangle_intersect(
41
+ ray_origin: vec3<f32>,
42
+ ray_dir: vec3<f32>,
43
+ v0: vec3<f32>,
44
+ v1: vec3<f32>,
45
+ v2: vec3<f32>
46
+ ) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, z: intersection_z)
47
+ let EPSILON = 0.0001;
48
+
49
+ // Calculate edges
50
+ let edge1 = v1 - v0;
51
+ let edge2 = v2 - v0;
52
+
53
+ // Cross product: ray_dir × edge2
54
+ let h = cross(ray_dir, edge2);
55
+
56
+ // Dot product: edge1 · h
57
+ let a = dot(edge1, h);
58
+
59
+ if (a > -EPSILON && a < EPSILON) {
60
+ return vec2<f32>(0.0, 0.0); // Ray parallel to triangle
61
+ }
62
+
63
+ let f = 1.0 / a;
64
+
65
+ // s = ray_origin - v0
66
+ let s = ray_origin - v0;
67
+
68
+ // u = f * (s · h)
69
+ let u = f * dot(s, h);
70
+
71
+ if (u < -EPSILON || u > 1.0 + EPSILON) {
72
+ return vec2<f32>(0.0, 0.0);
73
+ }
74
+
75
+ // Cross product: s × edge1
76
+ let q = cross(s, edge1);
77
+
78
+ // v = f * (ray_dir · q)
79
+ let v = f * dot(ray_dir, q);
80
+
81
+ if (v < -EPSILON || u + v > 1.0 + EPSILON) {
82
+ return vec2<f32>(0.0, 0.0);
83
+ }
84
+
85
+ // t = f * (edge2 · q)
86
+ let t = f * dot(edge2, q);
87
+
88
+ if (t > EPSILON) {
89
+ // Intersection found - return distance along ray (t parameter)
90
+ return vec2<f32>(1.0, t);
91
+ }
92
+
93
+ return vec2<f32>(0.0, 0.0);
94
+ }
95
+
96
+ @compute @workgroup_size(8, 8, 1)
97
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
98
+ let bucket_idx = global_id.z;
99
+ let grid_y = global_id.y;
100
+ let angle_idx = global_id.x;
101
+
102
+ // Bounds check
103
+ if (angle_idx >= uniforms.num_angles ||
104
+ grid_y >= uniforms.grid_y_height ||
105
+ bucket_idx >= uniforms.num_buckets) {
106
+ return;
107
+ }
108
+
109
+ let bucket = bucket_info[bucket_idx];
110
+ let angle = uniforms.start_angle + (f32(angle_idx) * uniforms.angle_step);
111
+
112
+ // Calculate bucket min grid X
113
+ let bucket_min_grid_x = u32((bucket.min_x - uniforms.global_min_x) / uniforms.resolution);
114
+
115
+ // Loop over X within this bucket
116
+ for (var local_x = 0u; local_x < uniforms.bucket_grid_width; local_x++) {
117
+ let grid_x = bucket_min_grid_x + local_x;
118
+ let world_x = uniforms.global_min_x + f32(grid_x) * uniforms.resolution;
119
+
120
+ // Rotating top-down scan: normal XY planar scan, rotated around X-axis
121
+ // Step 1: Define scan position in planar frame (X, Y, Z_above)
122
+ let scan_x = world_x;
123
+ let scan_y = f32(grid_y) * uniforms.resolution - uniforms.tool_width / 2.0;
124
+ let scan_z = uniforms.max_radius; // Start above the model
125
+
126
+ // Step 2: Rotate position (scan_x, scan_y, scan_z) around X-axis by 'angle'
127
+ // X stays the same, rotate YZ plane: y' = y*cos - z*sin, z' = y*sin + z*cos
128
+ let ray_origin_x = scan_x;
129
+ let ray_origin_y = scan_y * cos(angle) - scan_z * sin(angle);
130
+ let ray_origin_z = scan_y * sin(angle) + scan_z * cos(angle);
131
+ let ray_origin = vec3<f32>(ray_origin_x, ray_origin_y, ray_origin_z);
132
+
133
+ // Step 3: Rotate ray direction (0, 0, -1) around X-axis by 'angle'
134
+ // X component stays 0, rotate YZ: dy = 0*cos - (-1)*sin = sin, dz = 0*sin + (-1)*cos = -cos
135
+ let ray_dir = vec3<f32>(0.0, sin(angle), -cos(angle));
136
+
137
+ // Initialize best distance (closest hit)
138
+ var best_t: f32 = 1e10; // Start with very large distance
139
+ var found = false;
140
+
141
+ // Ray-cast against triangles in this bucket
142
+ for (var i = 0u; i < bucket.count; i++) {
143
+ let tri_idx = triangle_indices[bucket.start_index + i];
144
+ let tri_base = tri_idx * 9u;
145
+
146
+ // Read triangle vertices
147
+ let v0 = vec3<f32>(
148
+ triangles[tri_base],
149
+ triangles[tri_base + 1u],
150
+ triangles[tri_base + 2u]
151
+ );
152
+ let v1 = vec3<f32>(
153
+ triangles[tri_base + 3u],
154
+ triangles[tri_base + 4u],
155
+ triangles[tri_base + 5u]
156
+ );
157
+ let v2 = vec3<f32>(
158
+ triangles[tri_base + 6u],
159
+ triangles[tri_base + 7u],
160
+ triangles[tri_base + 8u]
161
+ );
162
+
163
+ let result = ray_triangle_intersect(ray_origin, ray_dir, v0, v1, v2);
164
+ let hit = result.x;
165
+ let t = result.y; // Distance along ray
166
+
167
+ if (hit > 0.5) {
168
+ // Keep closest hit (minimum t)
169
+ if (t < best_t) {
170
+ best_t = t;
171
+ found = true;
172
+ }
173
+ }
174
+ }
175
+
176
+ // Write output
177
+ // Layout: bucket_idx * numAngles * bucketWidth * gridHeight
178
+ // + angle_idx * bucketWidth * gridHeight
179
+ // + grid_y * bucketWidth
180
+ // + local_x
181
+ let output_idx = bucket_idx * uniforms.num_angles * uniforms.bucket_grid_width * uniforms.grid_y_height
182
+ + angle_idx * uniforms.bucket_grid_width * uniforms.grid_y_height
183
+ + grid_y * uniforms.bucket_grid_width
184
+ + local_x;
185
+
186
+ if (found) {
187
+ // Terrain height = distance from scan origin minus ray travel distance
188
+ // Ray started at max_radius from X-axis, traveled best_t distance to hit
189
+ let terrain_height = uniforms.max_radius - best_t;
190
+ output[output_idx] = terrain_height;
191
+ } else {
192
+ output[output_idx] = uniforms.z_floor;
193
+ }
194
+ }
195
+ }