@gridspace/raster-path 1.0.6 → 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.
- package/build/app.js +187 -12
- package/build/index.html +9 -2
- package/build/raster-path.js +125 -11
- package/build/raster-worker.js +952 -7
- package/package.json +6 -3
- package/src/core/path-radial-v3.js +405 -0
- package/src/core/path-radial.js +0 -1
- package/src/core/path-tracing.js +492 -0
- package/src/core/raster-config.js +37 -4
- package/src/core/raster-path.js +125 -11
- package/src/core/raster-worker.js +51 -0
- package/src/core/workload-calibrate.js +57 -3
- package/src/shaders/radial-rasterize-batched.wgsl +164 -0
- package/src/shaders/radial-rotate-triangles.wgsl +70 -0
- package/src/shaders/tracing-toolpath.wgsl +95 -0
- package/src/test/radial-v3-benchmark.cjs +184 -0
- package/src/test/radial-v3-bucket-test.cjs +154 -0
- package/src/test/tracing-test.cjs +307 -0
- package/src/web/app.js +187 -12
- package/src/web/index.html +9 -2
package/src/core/raster-path.js
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
/**
|
|
46
46
|
* Configuration options for RasterPath
|
|
47
47
|
* @typedef {Object} RasterPathConfig
|
|
48
|
-
* @property {'planar'|'radial'} mode - Rasterization mode (default: 'planar')
|
|
48
|
+
* @property {'planar'|'radial'|'tracing'} mode - Rasterization mode (default: 'planar')
|
|
49
49
|
* @property {boolean} autoTiling - Automatically tile large datasets (default: true)
|
|
50
50
|
* @property {number} gpuMemorySafetyMargin - Safety margin as percentage (default: 0.8 = 80%)
|
|
51
51
|
* @property {number} maxGPUMemoryMB - Maximum GPU memory per tile (default: 256MB)
|
|
@@ -80,8 +80,8 @@ export class RasterPath {
|
|
|
80
80
|
|
|
81
81
|
// Validate mode
|
|
82
82
|
const mode = config.mode || 'planar';
|
|
83
|
-
if (mode !== 'planar' && mode !== 'radial') {
|
|
84
|
-
throw new Error(`Invalid mode: ${mode}. Must be 'planar' or '
|
|
83
|
+
if (mode !== 'planar' && mode !== 'radial' && mode !== 'tracing') {
|
|
84
|
+
throw new Error(`Invalid mode: ${mode}. Must be 'planar', 'radial', or 'tracing'`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Validate rotationStep for radial mode
|
|
@@ -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
|
};
|
|
@@ -221,8 +222,8 @@ export class RasterPath {
|
|
|
221
222
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
if (this.mode === 'planar') {
|
|
225
|
-
// Planar: rasterize and return
|
|
225
|
+
if (this.mode === 'planar' || this.mode === 'tracing') {
|
|
226
|
+
// Planar/Tracing: rasterize and return (tracing reuses planar terrain rasterization)
|
|
226
227
|
const terrainData = await this.#rasterizePlanar({ triangles, zFloor, boundsOverride, isForTool: false, onProgress });
|
|
227
228
|
this.terrainData = terrainData;
|
|
228
229
|
return terrainData;
|
|
@@ -269,7 +270,7 @@ export class RasterPath {
|
|
|
269
270
|
* @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
|
|
270
271
|
* @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
|
|
271
272
|
*/
|
|
272
|
-
async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
|
|
273
|
+
async generateToolpaths({ xStep, yStep, zFloor, onProgress, paths, step }) {
|
|
273
274
|
if (!this.isInitialized) {
|
|
274
275
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
275
276
|
}
|
|
@@ -278,7 +279,7 @@ export class RasterPath {
|
|
|
278
279
|
throw new Error('Tool not loaded. Call loadTool() first.');
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
debug.log('gen.paths', { xStep, yStep, zFloor });
|
|
282
|
+
debug.log('gen.paths', { xStep, yStep, zFloor, paths: paths?.length, step });
|
|
282
283
|
|
|
283
284
|
if (this.mode === 'planar') {
|
|
284
285
|
if (!this.terrainData) {
|
|
@@ -292,7 +293,7 @@ export class RasterPath {
|
|
|
292
293
|
zFloor,
|
|
293
294
|
onProgress
|
|
294
295
|
});
|
|
295
|
-
} else {
|
|
296
|
+
} else if (this.mode === 'radial') {
|
|
296
297
|
// Radial mode: use stored triangles
|
|
297
298
|
if (!this.terrainTriangles) {
|
|
298
299
|
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
@@ -306,14 +307,83 @@ export class RasterPath {
|
|
|
306
307
|
zFloor: zFloor ?? this.terrainZFloor,
|
|
307
308
|
onProgress
|
|
308
309
|
});
|
|
310
|
+
} else if (this.mode === 'tracing') {
|
|
311
|
+
// Tracing mode: follow input paths
|
|
312
|
+
if (!this.terrainData) {
|
|
313
|
+
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
314
|
+
}
|
|
315
|
+
if (!paths || paths.length === 0) {
|
|
316
|
+
throw new Error('Tracing mode requires paths parameter (array of Float32Array XY coordinates)');
|
|
317
|
+
}
|
|
318
|
+
if (!step || step <= 0) {
|
|
319
|
+
throw new Error('Tracing mode requires step parameter (sampling resolution in world units)');
|
|
320
|
+
}
|
|
321
|
+
return this.#generateToolpathsTracing({
|
|
322
|
+
paths,
|
|
323
|
+
step,
|
|
324
|
+
zFloor,
|
|
325
|
+
onProgress
|
|
326
|
+
});
|
|
309
327
|
}
|
|
310
328
|
}
|
|
311
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Create reusable GPU buffers for tracing mode (optimization for iterative tracing)
|
|
332
|
+
* Call this after loadTerrain() and loadTool() to cache buffers across multiple trace calls
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async createTracingBuffers() {
|
|
336
|
+
if (this.mode !== 'tracing') {
|
|
337
|
+
throw new Error('createTracingBuffers() only available in tracing mode');
|
|
338
|
+
}
|
|
339
|
+
if (!this.terrainData || !this.toolData) {
|
|
340
|
+
throw new Error('Must call loadTerrain() and loadTool() before createTracingBuffers()');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const handler = () => resolve();
|
|
345
|
+
this.#sendMessage(
|
|
346
|
+
'create-tracing-buffers',
|
|
347
|
+
{
|
|
348
|
+
terrainPositions: this.terrainData.positions,
|
|
349
|
+
toolPositions: this.toolData.positions
|
|
350
|
+
},
|
|
351
|
+
'tracing-buffers-created',
|
|
352
|
+
handler
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Destroy reusable tracing buffers
|
|
359
|
+
* @returns {Promise<void>}
|
|
360
|
+
*/
|
|
361
|
+
async destroyTracingBuffers() {
|
|
362
|
+
if (this.mode !== 'tracing') {
|
|
363
|
+
return; // No-op for non-tracing modes
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
const handler = () => resolve();
|
|
368
|
+
this.#sendMessage(
|
|
369
|
+
'destroy-tracing-buffers',
|
|
370
|
+
{},
|
|
371
|
+
'tracing-buffers-destroyed',
|
|
372
|
+
handler
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
312
377
|
/**
|
|
313
378
|
* Terminate worker and cleanup resources
|
|
314
379
|
*/
|
|
315
|
-
terminate() {
|
|
380
|
+
async terminate() {
|
|
316
381
|
if (this.worker) {
|
|
382
|
+
// Cleanup tracing buffers if in tracing mode
|
|
383
|
+
if (this.mode === 'tracing') {
|
|
384
|
+
await this.destroyTracingBuffers();
|
|
385
|
+
}
|
|
386
|
+
|
|
317
387
|
this.worker.terminate();
|
|
318
388
|
this.worker = null;
|
|
319
389
|
this.isInitialized = false;
|
|
@@ -388,6 +458,46 @@ export class RasterPath {
|
|
|
388
458
|
});
|
|
389
459
|
}
|
|
390
460
|
|
|
461
|
+
async #generateToolpathsTracing({ paths, step, zFloor, onProgress }) {
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
// Set up progress handler if callback provided
|
|
464
|
+
if (onProgress) {
|
|
465
|
+
const progressHandler = (data) => {
|
|
466
|
+
onProgress(data.percent, { current: data.current, total: data.total, pathIndex: data.pathIndex });
|
|
467
|
+
};
|
|
468
|
+
this.messageHandlers.set('tracing-progress', progressHandler);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const handler = (data) => {
|
|
472
|
+
// Clean up progress handler
|
|
473
|
+
if (onProgress) {
|
|
474
|
+
this.messageHandlers.delete('tracing-progress');
|
|
475
|
+
}
|
|
476
|
+
resolve(data);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
this.#sendMessage(
|
|
480
|
+
'tracing-generate-toolpaths',
|
|
481
|
+
{
|
|
482
|
+
paths,
|
|
483
|
+
terrainPositions: this.terrainData.positions,
|
|
484
|
+
terrainData: {
|
|
485
|
+
width: this.terrainData.gridWidth,
|
|
486
|
+
height: this.terrainData.gridHeight,
|
|
487
|
+
bounds: this.terrainData.bounds
|
|
488
|
+
},
|
|
489
|
+
toolPositions: this.toolData.positions,
|
|
490
|
+
step,
|
|
491
|
+
gridStep: this.resolution,
|
|
492
|
+
terrainBounds: this.terrainData.bounds,
|
|
493
|
+
zFloor: zFloor ?? 0
|
|
494
|
+
},
|
|
495
|
+
'tracing-toolpaths-complete',
|
|
496
|
+
handler
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
391
501
|
async #generateToolpathsRadial({ triangles, bounds, toolData, xStep, yStep, zFloor, onProgress }) {
|
|
392
502
|
const maxRadius = this.#calculateMaxRadius(triangles);
|
|
393
503
|
|
|
@@ -422,9 +532,13 @@ export class RasterPath {
|
|
|
422
532
|
resolve(data);
|
|
423
533
|
};
|
|
424
534
|
|
|
425
|
-
// 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
|
+
|
|
426
540
|
this.#sendMessage(
|
|
427
|
-
|
|
541
|
+
messageType,
|
|
428
542
|
{
|
|
429
543
|
triangles: triangles,
|
|
430
544
|
bucketData,
|
|
@@ -54,6 +54,8 @@ 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';
|
|
58
|
+
import { generateTracingToolpaths, createReusableTracingBuffers, destroyReusableTracingBuffers } from './path-tracing.js';
|
|
57
59
|
import { calibrateGPU } from './workload-calibrate.js';
|
|
58
60
|
|
|
59
61
|
// Global error handler for uncaught errors in worker
|
|
@@ -127,6 +129,55 @@ self.onmessage = async function(e) {
|
|
|
127
129
|
}, toolpathTransferBuffers);
|
|
128
130
|
break;
|
|
129
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
|
+
|
|
141
|
+
case 'tracing-generate-toolpaths':
|
|
142
|
+
const tracingResult = await generateTracingToolpaths({
|
|
143
|
+
paths: data.paths,
|
|
144
|
+
terrainPositions: data.terrainPositions,
|
|
145
|
+
terrainData: data.terrainData,
|
|
146
|
+
toolPositions: data.toolPositions,
|
|
147
|
+
step: data.step,
|
|
148
|
+
gridStep: data.gridStep,
|
|
149
|
+
terrainBounds: data.terrainBounds,
|
|
150
|
+
zFloor: data.zFloor,
|
|
151
|
+
onProgress: (progressData) => {
|
|
152
|
+
self.postMessage({
|
|
153
|
+
type: 'tracing-progress',
|
|
154
|
+
data: progressData.data
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const tracingTransferBuffers = tracingResult.paths.map(p => p.buffer);
|
|
159
|
+
self.postMessage({
|
|
160
|
+
type: 'tracing-toolpaths-complete',
|
|
161
|
+
data: tracingResult
|
|
162
|
+
}, tracingTransferBuffers);
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case 'create-tracing-buffers':
|
|
166
|
+
createReusableTracingBuffers(data.terrainPositions, data.toolPositions);
|
|
167
|
+
self.postMessage({
|
|
168
|
+
type: 'tracing-buffers-created',
|
|
169
|
+
data: { success: true }
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'destroy-tracing-buffers':
|
|
174
|
+
destroyReusableTracingBuffers();
|
|
175
|
+
self.postMessage({
|
|
176
|
+
type: 'tracing-buffers-destroyed',
|
|
177
|
+
data: { success: true }
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
|
|
130
181
|
case 'calibrate':
|
|
131
182
|
const calibrationResult = await calibrateGPU(device, data?.options || {});
|
|
132
183
|
self.postMessage({
|
|
@@ -114,18 +114,72 @@ async function testWorkloadDispatch(device, pipeline, workgroupSize, triangleTes
|
|
|
114
114
|
passEncoder.dispatchWorkgroups(dispatchX, dispatchY, 1);
|
|
115
115
|
passEncoder.end();
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Submit the compute work
|
|
118
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
119
|
+
|
|
120
|
+
// TEST: Queue progress checkpoints while GPU works
|
|
121
|
+
const numCheckpoints = 5;
|
|
122
|
+
const checkpointInterval = 100; // ms
|
|
123
|
+
const progressSnapshots = [];
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < numCheckpoints; i++) {
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, checkpointInterval));
|
|
127
|
+
|
|
128
|
+
const checkpointStart = performance.now();
|
|
129
|
+
|
|
130
|
+
// Queue a copy to read progress
|
|
131
|
+
const checkpointEncoder = device.createCommandEncoder();
|
|
132
|
+
const checkpointStaging = device.createBuffer({
|
|
133
|
+
size: totalThreads * 4,
|
|
134
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
checkpointEncoder.copyBufferToBuffer(completionBuffer, 0, checkpointStaging, 0, totalThreads * 4);
|
|
138
|
+
device.queue.submit([checkpointEncoder.finish()]);
|
|
139
|
+
|
|
140
|
+
// Wait for this checkpoint copy to complete
|
|
141
|
+
await checkpointStaging.mapAsync(GPUMapMode.READ);
|
|
142
|
+
const checkpointElapsed = performance.now() - checkpointStart;
|
|
143
|
+
|
|
144
|
+
// Count completed threads
|
|
145
|
+
const checkpointData = new Uint32Array(checkpointStaging.getMappedRange());
|
|
146
|
+
let completedThreads = 0;
|
|
147
|
+
for (let j = 0; j < totalThreads; j++) {
|
|
148
|
+
if (checkpointData[j] === 1) completedThreads++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
progressSnapshots.push({
|
|
152
|
+
checkpoint: i + 1,
|
|
153
|
+
timeMs: Math.round(performance.now() - startTime),
|
|
154
|
+
completedThreads,
|
|
155
|
+
totalThreads,
|
|
156
|
+
percentComplete: Math.round((completedThreads / totalThreads) * 100),
|
|
157
|
+
checkpointLatencyMs: checkpointElapsed.toFixed(2)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
checkpointStaging.unmap();
|
|
161
|
+
checkpointStaging.destroy();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Final readback
|
|
118
165
|
const stagingBuffer = device.createBuffer({
|
|
119
166
|
size: totalThreads * 4,
|
|
120
167
|
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
121
168
|
});
|
|
122
169
|
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
const finalEncoder = device.createCommandEncoder();
|
|
171
|
+
finalEncoder.copyBufferToBuffer(completionBuffer, 0, stagingBuffer, 0, totalThreads * 4);
|
|
172
|
+
device.queue.submit([finalEncoder.finish()]);
|
|
125
173
|
|
|
126
174
|
await device.queue.onSubmittedWorkDone();
|
|
127
175
|
const elapsed = performance.now() - startTime;
|
|
128
176
|
|
|
177
|
+
// Log progress snapshots
|
|
178
|
+
if (progressSnapshots.length > 0) {
|
|
179
|
+
console.log('\n📊 Progress Checkpoints:');
|
|
180
|
+
console.table(progressSnapshots);
|
|
181
|
+
}
|
|
182
|
+
|
|
129
183
|
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
130
184
|
const completionData = new Uint32Array(stagingBuffer.getMappedRange());
|
|
131
185
|
const completionCopy = new Uint32Array(completionData);
|
|
@@ -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,95 @@
|
|
|
1
|
+
// Tracing toolpath generation
|
|
2
|
+
// Follows input polylines and calculates Z-depth at each sampled point
|
|
3
|
+
// Sentinel value for empty terrain cells (must match rasterize shader)
|
|
4
|
+
const EMPTY_CELL: f32 = -1e10;
|
|
5
|
+
const MAX_F32: f32 = 3.402823466e+38;
|
|
6
|
+
|
|
7
|
+
struct SparseToolPoint {
|
|
8
|
+
x_offset: i32,
|
|
9
|
+
y_offset: i32,
|
|
10
|
+
z_value: f32,
|
|
11
|
+
padding: f32,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct Uniforms {
|
|
15
|
+
terrain_width: u32,
|
|
16
|
+
terrain_height: u32,
|
|
17
|
+
tool_count: u32,
|
|
18
|
+
point_count: u32, // Number of sampled points to process
|
|
19
|
+
path_index: u32, // Index of current path being processed
|
|
20
|
+
terrain_min_x: f32, // Terrain bounding box (world coordinates)
|
|
21
|
+
terrain_min_y: f32,
|
|
22
|
+
grid_step: f32, // Resolution of terrain rasterization
|
|
23
|
+
oob_z: f32, // Z value for out-of-bounds points (zFloor)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@group(0) @binding(0) var<storage, read> terrain_map: array<f32>;
|
|
27
|
+
@group(0) @binding(1) var<storage, read> sparse_tool: array<SparseToolPoint>;
|
|
28
|
+
@group(0) @binding(2) var<storage, read> input_points: array<f32>; // XY pairs
|
|
29
|
+
@group(0) @binding(3) var<storage, read_write> output_depths: array<f32>; // Z values
|
|
30
|
+
@group(0) @binding(4) var<storage, read_write> max_z_buffer: array<atomic<i32>>; // Max Z per path (as bits)
|
|
31
|
+
@group(0) @binding(5) var<uniform> uniforms: Uniforms;
|
|
32
|
+
|
|
33
|
+
@compute @workgroup_size(64, 1, 1)
|
|
34
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
35
|
+
let point_idx = global_id.x;
|
|
36
|
+
|
|
37
|
+
if (point_idx >= uniforms.point_count) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Read input X,Y world coordinates
|
|
42
|
+
let world_x = input_points[point_idx * 2u + 0u];
|
|
43
|
+
let world_y = input_points[point_idx * 2u + 1u];
|
|
44
|
+
|
|
45
|
+
// Convert world coordinates to grid coordinates
|
|
46
|
+
let grid_x_f32 = (world_x - uniforms.terrain_min_x) / uniforms.grid_step;
|
|
47
|
+
let grid_y_f32 = (world_y - uniforms.terrain_min_y) / uniforms.grid_step;
|
|
48
|
+
let tool_center_x = i32(grid_x_f32);
|
|
49
|
+
let tool_center_y = i32(grid_y_f32);
|
|
50
|
+
|
|
51
|
+
// Check if tool center is outside terrain bounds
|
|
52
|
+
let center_oob = tool_center_x < 0 || tool_center_x >= i32(uniforms.terrain_width) ||
|
|
53
|
+
tool_center_y < 0 || tool_center_y >= i32(uniforms.terrain_height);
|
|
54
|
+
|
|
55
|
+
var max_collision_z = uniforms.oob_z;
|
|
56
|
+
var found_collision = false;
|
|
57
|
+
|
|
58
|
+
// Test each tool point for collision with terrain
|
|
59
|
+
for (var i = 0u; i < uniforms.tool_count; i++) {
|
|
60
|
+
let tool_point = sparse_tool[i];
|
|
61
|
+
let terrain_x = tool_center_x + tool_point.x_offset;
|
|
62
|
+
let terrain_y = tool_center_y + tool_point.y_offset;
|
|
63
|
+
|
|
64
|
+
// Bounds check: terrain sample must be within terrain grid
|
|
65
|
+
if (terrain_x < 0 || terrain_x >= i32(uniforms.terrain_width) ||
|
|
66
|
+
terrain_y < 0 || terrain_y >= i32(uniforms.terrain_height)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let terrain_idx = u32(terrain_y) * uniforms.terrain_width + u32(terrain_x);
|
|
71
|
+
let terrain_z = terrain_map[terrain_idx];
|
|
72
|
+
|
|
73
|
+
// Check if terrain cell has geometry (not empty sentinel value)
|
|
74
|
+
if (terrain_z > EMPTY_CELL + 1.0) {
|
|
75
|
+
// Tool z_value is positive offset from tip (tip=0, shaft=+50)
|
|
76
|
+
// Add to terrain height to find where tool center needs to be
|
|
77
|
+
let collision_z = terrain_z + tool_point.z_value;
|
|
78
|
+
max_collision_z = max(max_collision_z, collision_z);
|
|
79
|
+
found_collision = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If no collision found and center was in-bounds, use oob_z
|
|
84
|
+
var output_z = uniforms.oob_z;
|
|
85
|
+
if (found_collision) {
|
|
86
|
+
output_z = max_collision_z;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
output_depths[point_idx] = output_z;
|
|
90
|
+
|
|
91
|
+
// Update max Z for this path using atomic operation
|
|
92
|
+
// Convert float to int bits for atomic comparison
|
|
93
|
+
let z_bits = bitcast<i32>(output_z);
|
|
94
|
+
atomicMax(&max_z_buffer[uniforms.path_index], z_bits);
|
|
95
|
+
}
|