@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,405 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════════════
3
+ * Path Radial V3 - Bucket-Angle Pipeline with Y-Filtering
4
+ * ═══════════════════════════════════════════════════════════════════════════
5
+ *
6
+ * ALGORITHM CHANGES FROM V2:
7
+ * ──────────────────────────
8
+ * V2 (current): Process all angles simultaneously
9
+ * - Rotate rays on the fly
10
+ * - Test all triangles in bucket (no Y-filtering)
11
+ * - Large memory footprint: numAngles × gridWidth × gridHeight
12
+ *
13
+ * V3 (this file): Process bucket-by-angle pipeline
14
+ * For each bucket:
15
+ * For each angle:
16
+ * 1. Rotate triangles (parallel) → rotated tris + Y-bounds
17
+ * 2. Rasterize with Y-filter (parallel) → dense terrain strip
18
+ * 3. Toolpath generation (parallel) → sparse toolpath
19
+ *
20
+ * BENEFITS:
21
+ * ─────────
22
+ * - Lower memory: Only one angle's data in GPU at a time
23
+ * - Y-axis filtering: Skip triangles outside tool radius
24
+ * - Immediate toolpath generation: No need to store all strips
25
+ * - Better cache locality: Process bucket completely before moving on
26
+ *
27
+ * TODO: Memory Safety
28
+ * ───────────────────
29
+ * V3 currently does NOT have memory safety checks like V2 does. V2 batches angles
30
+ * if total memory (numAngles × gridWidth × gridHeight × 4) exceeds 1800MB.
31
+ *
32
+ * V3 processes one angle at a time (inherently lower memory), but doesn't check if:
33
+ * - Triangle input buffer (triangles.byteLength) exceeds GPU limits
34
+ * - Rotated triangles buffer (numTriangles × 11 × 4) exceeds GPU limits
35
+ * - Output raster buffer (fullGridWidth × gridHeight × 4) exceeds GPU limits
36
+ *
37
+ * This could cause crashes on extremely large models with millions of triangles.
38
+ * Consider adding checks and batching for triangle buffers if needed.
39
+ *
40
+ * ═══════════════════════════════════════════════════════════════════════════
41
+ */
42
+
43
+ import {
44
+ device, config, debug, diagnostic,
45
+ cachedRadialV3RotatePipeline,
46
+ cachedRadialV3BatchedRasterizePipeline
47
+ } from './raster-config.js';
48
+ import {
49
+ createReusableToolpathBuffers,
50
+ destroyReusableToolpathBuffers,
51
+ runToolpathComputeWithBuffers
52
+ } from './path-planar.js';
53
+ import { createSparseToolFromPoints } from './raster-tool.js';
54
+
55
+ /**
56
+ * Rotate all triangles in a bucket by a single angle
57
+ */
58
+ async function rotateTriangles({
59
+ triangleBuffer, // GPU buffer with original triangles
60
+ numTriangles,
61
+ angle // Radians
62
+ }) {
63
+ const rotatePipeline = cachedRadialV3RotatePipeline;
64
+ if (!rotatePipeline) {
65
+ throw new Error('Radial V3 pipelines not initialized');
66
+ }
67
+
68
+ // Create output buffer for rotated triangles + bounds
69
+ // Layout: 11 floats per triangle (v0, v1, v2, y_min, y_max)
70
+ const outputSize = numTriangles * 11 * 4;
71
+ const rotatedBuffer = device.createBuffer({
72
+ size: outputSize,
73
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
74
+ });
75
+
76
+ // Create uniforms
77
+ const uniformBuffer = device.createBuffer({
78
+ size: 8, // f32 angle + u32 num_triangles
79
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
80
+ mappedAtCreation: true
81
+ });
82
+
83
+ const uniformView = new ArrayBuffer(8);
84
+ const floatView = new Float32Array(uniformView);
85
+ const uintView = new Uint32Array(uniformView);
86
+ floatView[0] = angle;
87
+ uintView[1] = numTriangles;
88
+
89
+ new Uint8Array(uniformBuffer.getMappedRange()).set(new Uint8Array(uniformView));
90
+ uniformBuffer.unmap();
91
+
92
+ // Create bind group
93
+ const bindGroup = device.createBindGroup({
94
+ layout: rotatePipeline.getBindGroupLayout(0),
95
+ entries: [
96
+ { binding: 0, resource: { buffer: triangleBuffer } },
97
+ { binding: 1, resource: { buffer: rotatedBuffer } },
98
+ { binding: 2, resource: { buffer: uniformBuffer } }
99
+ ]
100
+ });
101
+
102
+ // Dispatch
103
+ const commandEncoder = device.createCommandEncoder();
104
+ const passEncoder = commandEncoder.beginComputePass();
105
+ passEncoder.setPipeline(rotatePipeline);
106
+ passEncoder.setBindGroup(0, bindGroup);
107
+ passEncoder.dispatchWorkgroups(Math.ceil(numTriangles / 64));
108
+ passEncoder.end();
109
+
110
+ device.queue.submit([commandEncoder.finish()]);
111
+
112
+ // Cleanup
113
+ uniformBuffer.destroy();
114
+
115
+ return rotatedBuffer;
116
+ }
117
+
118
+ /**
119
+ * Rasterize ALL buckets in one dispatch (batched GPU processing)
120
+ */
121
+ async function rasterizeAllBuckets({
122
+ rotatedTrianglesBuffer,
123
+ buckets,
124
+ triangleIndices,
125
+ resolution,
126
+ toolRadius,
127
+ fullGridWidth,
128
+ gridHeight,
129
+ globalMinX,
130
+ bucketMinY,
131
+ zFloor
132
+ }) {
133
+ const rasterizePipeline = cachedRadialV3BatchedRasterizePipeline;
134
+ if (!rasterizePipeline) {
135
+ throw new Error('Radial V3 batched pipeline not initialized');
136
+ }
137
+
138
+ // Create bucket info buffer (all buckets)
139
+ const bucketInfoSize = buckets.length * 16; // 4 fields × 4 bytes per bucket
140
+ const bucketInfoBuffer = device.createBuffer({
141
+ size: bucketInfoSize,
142
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
143
+ mappedAtCreation: true
144
+ });
145
+
146
+ const bucketView = new ArrayBuffer(bucketInfoSize);
147
+ const bucketFloatView = new Float32Array(bucketView);
148
+ const bucketUintView = new Uint32Array(bucketView);
149
+
150
+ for (let i = 0; i < buckets.length; i++) {
151
+ const bucket = buckets[i];
152
+ const offset = i * 4;
153
+ bucketFloatView[offset] = bucket.minX;
154
+ bucketFloatView[offset + 1] = bucket.maxX;
155
+ bucketUintView[offset + 2] = bucket.startIndex;
156
+ bucketUintView[offset + 3] = bucket.count;
157
+ }
158
+
159
+ new Uint8Array(bucketInfoBuffer.getMappedRange()).set(new Uint8Array(bucketView));
160
+ bucketInfoBuffer.unmap();
161
+
162
+ // Create triangle indices buffer
163
+ const indicesBuffer = device.createBuffer({
164
+ size: triangleIndices.byteLength,
165
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
166
+ mappedAtCreation: true
167
+ });
168
+ new Uint32Array(indicesBuffer.getMappedRange()).set(triangleIndices);
169
+ indicesBuffer.unmap();
170
+
171
+ // Create output buffer for full terrain strip
172
+ const outputSize = fullGridWidth * gridHeight * 4;
173
+ const outputBuffer = device.createBuffer({
174
+ size: outputSize,
175
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
176
+ });
177
+
178
+ // Initialize with zFloor
179
+ const initData = new Float32Array(fullGridWidth * gridHeight);
180
+ initData.fill(zFloor);
181
+ device.queue.writeBuffer(outputBuffer, 0, initData);
182
+
183
+ // Create uniforms
184
+ const uniformBuffer = device.createBuffer({
185
+ size: 32, // 8 fields × 4 bytes
186
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
187
+ mappedAtCreation: true
188
+ });
189
+
190
+ const uniformView = new ArrayBuffer(32);
191
+ const floatView = new Float32Array(uniformView);
192
+ const uintView = new Uint32Array(uniformView);
193
+
194
+ floatView[0] = resolution;
195
+ floatView[1] = toolRadius;
196
+ uintView[2] = fullGridWidth;
197
+ uintView[3] = gridHeight;
198
+ floatView[4] = globalMinX;
199
+ floatView[5] = bucketMinY;
200
+ floatView[6] = zFloor;
201
+ uintView[7] = buckets.length;
202
+
203
+ new Uint8Array(uniformBuffer.getMappedRange()).set(new Uint8Array(uniformView));
204
+ uniformBuffer.unmap();
205
+
206
+ // Create bind group
207
+ const bindGroup = device.createBindGroup({
208
+ layout: rasterizePipeline.getBindGroupLayout(0),
209
+ entries: [
210
+ { binding: 0, resource: { buffer: rotatedTrianglesBuffer } },
211
+ { binding: 1, resource: { buffer: outputBuffer } },
212
+ { binding: 2, resource: { buffer: uniformBuffer } },
213
+ { binding: 3, resource: { buffer: bucketInfoBuffer } },
214
+ { binding: 4, resource: { buffer: indicesBuffer } }
215
+ ]
216
+ });
217
+
218
+ // Dispatch - covers full grid width (all buckets)
219
+ const commandEncoder = device.createCommandEncoder();
220
+ const passEncoder = commandEncoder.beginComputePass();
221
+ passEncoder.setPipeline(rasterizePipeline);
222
+ passEncoder.setBindGroup(0, bindGroup);
223
+
224
+ const dispatchX = Math.ceil(fullGridWidth / 8);
225
+ const dispatchY = Math.ceil(gridHeight / 8);
226
+ passEncoder.dispatchWorkgroups(dispatchX, dispatchY);
227
+ passEncoder.end();
228
+
229
+ // Read back results
230
+ const stagingBuffer = device.createBuffer({
231
+ size: outputSize,
232
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
233
+ });
234
+
235
+ commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputSize);
236
+ device.queue.submit([commandEncoder.finish()]);
237
+
238
+ await device.queue.onSubmittedWorkDone();
239
+ await stagingBuffer.mapAsync(GPUMapMode.READ);
240
+ const terrainData = new Float32Array(stagingBuffer.getMappedRange().slice());
241
+ stagingBuffer.unmap();
242
+
243
+ // Cleanup
244
+ outputBuffer.destroy();
245
+ stagingBuffer.destroy();
246
+ uniformBuffer.destroy();
247
+ bucketInfoBuffer.destroy();
248
+ indicesBuffer.destroy();
249
+
250
+ return terrainData;
251
+ }
252
+ /**
253
+ * Generate radial toolpaths using bucket-angle pipeline
254
+ */
255
+ export async function generateRadialToolpathsV3({
256
+ triangles,
257
+ bucketData,
258
+ toolData,
259
+ resolution,
260
+ angleStep,
261
+ numAngles,
262
+ maxRadius,
263
+ toolWidth,
264
+ zFloor,
265
+ bounds,
266
+ xStep,
267
+ yStep
268
+ }) {
269
+ debug.log('radial-v3-generate-toolpaths', { triangles: triangles.length / 9, numAngles, resolution });
270
+
271
+ const pipelineStartTime = performance.now();
272
+ const allStripToolpaths = [];
273
+ let totalToolpathPoints = 0;
274
+
275
+ // Prepare sparse tool once
276
+ const sparseToolData = createSparseToolFromPoints(toolData.positions);
277
+ debug.log(`Created sparse tool: ${sparseToolData.count} points (reusing for all strips)`);
278
+
279
+ const toolRadius = toolWidth / 2;
280
+
281
+ // Calculate full grid dimensions (all buckets)
282
+ const bucketMinX = bucketData.buckets[0].minX;
283
+ const bucketMaxX = bucketData.buckets[bucketData.numBuckets - 1].maxX;
284
+ const fullWidth = bucketMaxX - bucketMinX;
285
+ const fullGridWidth = Math.ceil(fullWidth / resolution);
286
+ const gridHeight = Math.ceil(toolWidth / resolution);
287
+
288
+ // OPTIMIZATION: Upload all triangles to GPU ONCE (reused across all angles)
289
+ debug.log(`Uploading ${triangles.length / 9} triangles to GPU (reused across all angles)...`);
290
+ const allTrianglesBuffer = device.createBuffer({
291
+ size: triangles.byteLength,
292
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
293
+ mappedAtCreation: true
294
+ });
295
+ new Float32Array(allTrianglesBuffer.getMappedRange()).set(triangles);
296
+ allTrianglesBuffer.unmap();
297
+
298
+ // Process angle-by-angle (outer loop)
299
+ for (let angleIdx = 0; angleIdx < numAngles; angleIdx++) {
300
+ const angle = -(angleIdx * angleStep * (Math.PI / 180)); // Convert to radians (negative: rotating terrain vs tool)
301
+ const angleDegrees = angleIdx * angleStep;
302
+
303
+ if (diagnostic) {
304
+ debug.log(`Angle ${angleIdx + 1}/${numAngles}: ${angleDegrees.toFixed(1)}°`);
305
+ }
306
+
307
+ // Report progress
308
+ if (angleIdx % 10 === 0 || angleIdx === numAngles - 1) {
309
+ const stripProgress = ((angleIdx + 1) / numAngles) * 98;
310
+ self.postMessage({
311
+ type: 'toolpath-progress',
312
+ data: {
313
+ percent: Math.round(stripProgress),
314
+ current: angleIdx + 1,
315
+ total: numAngles,
316
+ layer: angleIdx + 1
317
+ }
318
+ });
319
+ }
320
+
321
+ // OPTIMIZATION: Rotate ALL triangles once per angle (batch rotation)
322
+ const numTotalTriangles = triangles.length / 9;
323
+ const allRotatedTrianglesBuffer = await rotateTriangles({
324
+ triangleBuffer: allTrianglesBuffer,
325
+ numTriangles: numTotalTriangles,
326
+ angle
327
+ });
328
+
329
+ // OPTIMIZATION: Rasterize ALL buckets in ONE dispatch (no CPU loop!)
330
+ const fullTerrainStrip = await rasterizeAllBuckets({
331
+ rotatedTrianglesBuffer: allRotatedTrianglesBuffer,
332
+ buckets: bucketData.buckets,
333
+ triangleIndices: bucketData.triangleIndices,
334
+ resolution,
335
+ toolRadius,
336
+ fullGridWidth,
337
+ gridHeight,
338
+ globalMinX: bucketMinX,
339
+ bucketMinY: -toolWidth / 2,
340
+ zFloor
341
+ });
342
+
343
+ // Cleanup rotated buffer (created per angle)
344
+ allRotatedTrianglesBuffer.destroy();
345
+
346
+ // Step 3: Generate toolpath for this complete angle strip
347
+ const reusableToolpathBuffers = createReusableToolpathBuffers(
348
+ fullGridWidth,
349
+ gridHeight,
350
+ sparseToolData,
351
+ xStep,
352
+ gridHeight
353
+ );
354
+
355
+ const stripToolpathResult = await runToolpathComputeWithBuffers(
356
+ fullTerrainStrip,
357
+ fullGridWidth,
358
+ gridHeight,
359
+ xStep,
360
+ gridHeight,
361
+ zFloor,
362
+ reusableToolpathBuffers,
363
+ pipelineStartTime
364
+ );
365
+
366
+ destroyReusableToolpathBuffers(reusableToolpathBuffers);
367
+
368
+ allStripToolpaths.push({
369
+ angle: angleDegrees,
370
+ pathData: stripToolpathResult.pathData,
371
+ numScanlines: stripToolpathResult.numScanlines,
372
+ pointsPerLine: stripToolpathResult.pointsPerLine,
373
+ terrainBounds: {
374
+ min: { x: bucketMinX, y: -toolWidth / 2, z: zFloor },
375
+ max: { x: bucketMaxX, y: toolWidth / 2, z: bounds.max.z }
376
+ }
377
+ });
378
+
379
+ totalToolpathPoints += stripToolpathResult.pathData.length;
380
+ }
381
+
382
+ // Cleanup triangles buffer (reused across all angles)
383
+ allTrianglesBuffer.destroy();
384
+ debug.log(`Destroyed reusable triangle buffer`);
385
+
386
+ const pipelineTotalTime = performance.now() - pipelineStartTime;
387
+ debug.log(`Complete radial V3 toolpath: ${allStripToolpaths.length} strips, ${totalToolpathPoints} total points in ${pipelineTotalTime.toFixed(0)}ms`);
388
+
389
+ // Send final 100% progress
390
+ self.postMessage({
391
+ type: 'toolpath-progress',
392
+ data: {
393
+ percent: 100,
394
+ current: bucketData.numBuckets * numAngles,
395
+ total: bucketData.numBuckets * numAngles,
396
+ layer: numAngles
397
+ }
398
+ });
399
+
400
+ return {
401
+ strips: allStripToolpaths,
402
+ totalPoints: totalToolpathPoints,
403
+ numStrips: allStripToolpaths.length
404
+ };
405
+ }
@@ -65,6 +65,10 @@ export let cachedRadialBatchPipeline = null;
65
65
  export let cachedRadialBatchShaderModule = null;
66
66
  export let cachedTracingPipeline = null;
67
67
  export let cachedTracingShaderModule = null;
68
+ export let cachedRadialV3RotatePipeline = null;
69
+ export let cachedRadialV3RotateShaderModule = null;
70
+ export let cachedRadialV3BatchedRasterizePipeline = null;
71
+ export let cachedRadialV3BatchedRasterizeShaderModule = null;
68
72
 
69
73
  // Constants
70
74
  export const EMPTY_CELL = -1e10;
@@ -97,6 +101,8 @@ const rasterizeShaderCode = 'SHADER:planar-rasterize';
97
101
  const toolpathShaderCode = 'SHADER:planar-toolpath';
98
102
  const radialRasterizeShaderCode = 'SHADER:radial-raster';
99
103
  const tracingShaderCode = 'SHADER:tracing-toolpath';
104
+ const radialV3RotateShaderCode = 'SHADER:radial-rotate-triangles';
105
+ const radialV3BatchedRasterizeShaderCode = 'SHADER:radial-rasterize-batched';
100
106
 
101
107
  // Initialize WebGPU device in worker context
102
108
  export async function initWebGPU() {
@@ -167,6 +173,24 @@ export async function initWebGPU() {
167
173
  compute: { module: cachedTracingShaderModule, entryPoint: 'main' },
168
174
  });
169
175
 
176
+ // Pre-compile radial V3 rotate shader module
177
+ cachedRadialV3RotateShaderModule = device.createShaderModule({ code: radialV3RotateShaderCode });
178
+
179
+ // Pre-create radial V3 rotate pipeline
180
+ cachedRadialV3RotatePipeline = device.createComputePipeline({
181
+ layout: 'auto',
182
+ compute: { module: cachedRadialV3RotateShaderModule, entryPoint: 'main' },
183
+ });
184
+
185
+ // Pre-compile radial V3 batched rasterize shader module
186
+ cachedRadialV3BatchedRasterizeShaderModule = device.createShaderModule({ code: radialV3BatchedRasterizeShaderCode });
187
+
188
+ // Pre-create radial V3 batched rasterize pipeline
189
+ cachedRadialV3BatchedRasterizePipeline = device.createComputePipeline({
190
+ layout: 'auto',
191
+ compute: { module: cachedRadialV3BatchedRasterizeShaderModule, entryPoint: 'main' },
192
+ });
193
+
170
194
  // Store device capabilities
171
195
  deviceCapabilities = {
172
196
  maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
@@ -111,6 +111,7 @@ export class RasterPath {
111
111
  gpuMemorySafetyMargin: config.gpuMemorySafetyMargin ?? 0.8,
112
112
  autoTiling: config.autoTiling ?? true,
113
113
  batchDivisor: config.batchDivisor ?? 1, // For testing batching overhead
114
+ radialV3: config.radialV3 ?? false, // Use radial V3 pipeline (rotate-filter-toolpath)
114
115
  debug: config.debug,
115
116
  quiet: config.quiet
116
117
  };
@@ -531,9 +532,13 @@ export class RasterPath {
531
532
  resolve(data);
532
533
  };
533
534
 
534
- // Send entire pipeline to worker
535
+ // Send entire pipeline to worker (use V3 if configured)
536
+ const messageType = this.config.radialV3
537
+ ? 'radial-generate-toolpaths-v3'
538
+ : 'radial-generate-toolpaths';
539
+
535
540
  this.#sendMessage(
536
- 'radial-generate-toolpaths',
541
+ messageType,
537
542
  {
538
543
  triangles: triangles,
539
544
  bucketData,
@@ -54,6 +54,7 @@ import { initWebGPU, setConfig, updateConfig, deviceCapabilities, debug, device
54
54
  import { rasterizeMesh } from './raster-planar.js';
55
55
  import { generateToolpath } from './path-planar.js';
56
56
  import { generateRadialToolpaths } from './path-radial.js';
57
+ import { generateRadialToolpathsV3 } from './path-radial-v3.js';
57
58
  import { generateTracingToolpaths, createReusableTracingBuffers, destroyReusableTracingBuffers } from './path-tracing.js';
58
59
  import { calibrateGPU } from './workload-calibrate.js';
59
60
 
@@ -128,6 +129,15 @@ self.onmessage = async function(e) {
128
129
  }, toolpathTransferBuffers);
129
130
  break;
130
131
 
132
+ case 'radial-generate-toolpaths-v3':
133
+ const radialV3ToolpathResult = await generateRadialToolpathsV3(data);
134
+ const v3ToolpathTransferBuffers = radialV3ToolpathResult.strips.map(strip => strip.pathData.buffer);
135
+ self.postMessage({
136
+ type: 'radial-toolpaths-complete',
137
+ data: radialV3ToolpathResult
138
+ }, v3ToolpathTransferBuffers);
139
+ break;
140
+
131
141
  case 'tracing-generate-toolpaths':
132
142
  const tracingResult = await generateTracingToolpaths({
133
143
  paths: data.paths,
@@ -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
+ }