@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.
- package/build/app.js +36 -0
- package/build/index.html +3 -0
- package/build/raster-path.js +7 -2
- package/build/raster-worker.js +533 -2
- package/package.json +3 -2
- package/src/core/path-radial-v3.js +405 -0
- package/src/core/raster-config.js +24 -0
- package/src/core/raster-path.js +7 -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,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,
|
package/src/core/raster-path.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|