@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
package/build/raster-worker.js
CHANGED
|
@@ -11,6 +11,10 @@ var cachedRadialBatchPipeline = null;
|
|
|
11
11
|
var cachedRadialBatchShaderModule = null;
|
|
12
12
|
var cachedTracingPipeline = null;
|
|
13
13
|
var cachedTracingShaderModule = null;
|
|
14
|
+
var cachedRadialV3RotatePipeline = null;
|
|
15
|
+
var cachedRadialV3RotateShaderModule = null;
|
|
16
|
+
var cachedRadialV3BatchedRasterizePipeline = null;
|
|
17
|
+
var cachedRadialV3BatchedRasterizeShaderModule = null;
|
|
14
18
|
var EMPTY_CELL = -1e10;
|
|
15
19
|
var log_pre = "[Worker]";
|
|
16
20
|
var diagnostic = false;
|
|
@@ -633,6 +637,242 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
|
633
637
|
atomicMax(&max_z_buffer[uniforms.path_index], z_bits);
|
|
634
638
|
}
|
|
635
639
|
`;
|
|
640
|
+
var radialV3RotateShaderCode = `// Triangle rotation shader for radial rasterization V3
|
|
641
|
+
// Rotates all triangles in a bucket by a single angle and computes Y-bounds
|
|
642
|
+
|
|
643
|
+
struct Uniforms {
|
|
644
|
+
angle: f32, // Rotation angle in radians
|
|
645
|
+
num_triangles: u32, // Number of triangles to rotate
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
struct RotatedTriangle {
|
|
649
|
+
v0: vec3<f32>, // Rotated vertex 0
|
|
650
|
+
v1: vec3<f32>, // Rotated vertex 1
|
|
651
|
+
v2: vec3<f32>, // Rotated vertex 2
|
|
652
|
+
y_min: f32, // Minimum Y coordinate (for filtering)
|
|
653
|
+
y_max: f32, // Maximum Y coordinate (for filtering)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
@group(0) @binding(0) var<storage, read> triangles: array<f32>; // Input: original triangles (flat array)
|
|
657
|
+
@group(0) @binding(1) var<storage, read_write> rotated: array<f32>; // Output: rotated triangles + bounds
|
|
658
|
+
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
659
|
+
|
|
660
|
+
// Rotate a point around X-axis
|
|
661
|
+
fn rotate_around_x(p: vec3<f32>, angle: f32) -> vec3<f32> {
|
|
662
|
+
let cos_a = cos(angle);
|
|
663
|
+
let sin_a = sin(angle);
|
|
664
|
+
|
|
665
|
+
return vec3<f32>(
|
|
666
|
+
p.x,
|
|
667
|
+
p.y * cos_a - p.z * sin_a,
|
|
668
|
+
p.y * sin_a + p.z * cos_a
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
@compute @workgroup_size(64, 1, 1)
|
|
673
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
674
|
+
let tri_idx = global_id.x;
|
|
675
|
+
|
|
676
|
+
if (tri_idx >= uniforms.num_triangles) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Read original triangle vertices
|
|
681
|
+
let base = tri_idx * 9u;
|
|
682
|
+
let v0 = vec3<f32>(triangles[base], triangles[base + 1u], triangles[base + 2u]);
|
|
683
|
+
let v1 = vec3<f32>(triangles[base + 3u], triangles[base + 4u], triangles[base + 5u]);
|
|
684
|
+
let v2 = vec3<f32>(triangles[base + 6u], triangles[base + 7u], triangles[base + 8u]);
|
|
685
|
+
|
|
686
|
+
// Rotate vertices around X-axis
|
|
687
|
+
let v0_rot = rotate_around_x(v0, uniforms.angle);
|
|
688
|
+
let v1_rot = rotate_around_x(v1, uniforms.angle);
|
|
689
|
+
let v2_rot = rotate_around_x(v2, uniforms.angle);
|
|
690
|
+
|
|
691
|
+
// Compute Y bounds for fast filtering during rasterization
|
|
692
|
+
let y_min = min(v0_rot.y, min(v1_rot.y, v2_rot.y));
|
|
693
|
+
let y_max = max(v0_rot.y, max(v1_rot.y, v2_rot.y));
|
|
694
|
+
|
|
695
|
+
// Write rotated triangle + bounds
|
|
696
|
+
// Layout: 11 floats per triangle: v0(3), v1(3), v2(3), y_min(1), y_max(1)
|
|
697
|
+
let out_base = tri_idx * 11u;
|
|
698
|
+
rotated[out_base] = v0_rot.x;
|
|
699
|
+
rotated[out_base + 1u] = v0_rot.y;
|
|
700
|
+
rotated[out_base + 2u] = v0_rot.z;
|
|
701
|
+
rotated[out_base + 3u] = v1_rot.x;
|
|
702
|
+
rotated[out_base + 4u] = v1_rot.y;
|
|
703
|
+
rotated[out_base + 5u] = v1_rot.z;
|
|
704
|
+
rotated[out_base + 6u] = v2_rot.x;
|
|
705
|
+
rotated[out_base + 7u] = v2_rot.y;
|
|
706
|
+
rotated[out_base + 8u] = v2_rot.z;
|
|
707
|
+
rotated[out_base + 9u] = y_min;
|
|
708
|
+
rotated[out_base + 10u] = y_max;
|
|
709
|
+
}
|
|
710
|
+
`;
|
|
711
|
+
var radialV3BatchedRasterizeShaderCode = `// Radial V3 batched bucket rasterization
|
|
712
|
+
// Processes ALL buckets in one dispatch - GPU threads find their bucket
|
|
713
|
+
|
|
714
|
+
const EPSILON: f32 = 0.0001;
|
|
715
|
+
|
|
716
|
+
struct Uniforms {
|
|
717
|
+
resolution: f32, // Grid step size (mm)
|
|
718
|
+
tool_radius: f32, // Tool radius for Y-filtering
|
|
719
|
+
full_grid_width: u32, // Full grid width (all buckets)
|
|
720
|
+
grid_height: u32, // Number of Y cells
|
|
721
|
+
global_min_x: f32, // Global minimum X coordinate
|
|
722
|
+
bucket_min_y: f32, // Y-axis start (typically -tool_width/2)
|
|
723
|
+
z_floor: f32, // Z value for empty cells
|
|
724
|
+
num_buckets: u32, // Number of buckets
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
struct BucketInfo {
|
|
728
|
+
min_x: f32, // Bucket X range start
|
|
729
|
+
max_x: f32, // Bucket X range end
|
|
730
|
+
start_index: u32, // Index into triangle_indices array
|
|
731
|
+
count: u32, // Number of triangles in this bucket
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
@group(0) @binding(0) var<storage, read> rotated_triangles: array<f32>; // ALL rotated triangles + bounds
|
|
735
|
+
@group(0) @binding(1) var<storage, read_write> output: array<f32>; // Full-width output grid
|
|
736
|
+
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
737
|
+
@group(0) @binding(3) var<storage, read> all_buckets: array<BucketInfo>; // All bucket descriptors
|
|
738
|
+
@group(0) @binding(4) var<storage, read> triangle_indices: array<u32>; // All triangle indices
|
|
739
|
+
|
|
740
|
+
// Simplified ray-triangle intersection for downward rays
|
|
741
|
+
fn ray_triangle_intersect_downward(
|
|
742
|
+
ray_origin: vec3<f32>,
|
|
743
|
+
v0: vec3<f32>,
|
|
744
|
+
v1: vec3<f32>,
|
|
745
|
+
v2: vec3<f32>
|
|
746
|
+
) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, t: distance along ray)
|
|
747
|
+
let ray_dir = vec3<f32>(0.0, 0.0, -1.0);
|
|
748
|
+
|
|
749
|
+
let edge1 = v1 - v0;
|
|
750
|
+
let edge2 = v2 - v0;
|
|
751
|
+
let h = cross(ray_dir, edge2);
|
|
752
|
+
let a = dot(edge1, h);
|
|
753
|
+
|
|
754
|
+
if (a > -EPSILON && a < EPSILON) {
|
|
755
|
+
return vec2<f32>(0.0, 0.0);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let f = 1.0 / a;
|
|
759
|
+
let s = ray_origin - v0;
|
|
760
|
+
let u = f * dot(s, h);
|
|
761
|
+
|
|
762
|
+
if (u < -EPSILON || u > 1.0 + EPSILON) {
|
|
763
|
+
return vec2<f32>(0.0, 0.0);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
let q = cross(s, edge1);
|
|
767
|
+
let v = f * dot(ray_dir, q);
|
|
768
|
+
|
|
769
|
+
if (v < -EPSILON || u + v > 1.0 + EPSILON) {
|
|
770
|
+
return vec2<f32>(0.0, 0.0);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
let t = f * dot(edge2, q);
|
|
774
|
+
|
|
775
|
+
if (t > EPSILON) {
|
|
776
|
+
return vec2<f32>(1.0, t);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return vec2<f32>(0.0, 0.0);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
@compute @workgroup_size(8, 8, 1)
|
|
783
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
784
|
+
let grid_x = global_id.x;
|
|
785
|
+
let grid_y = global_id.y;
|
|
786
|
+
|
|
787
|
+
// Bounds check
|
|
788
|
+
if (grid_x >= uniforms.full_grid_width || grid_y >= uniforms.grid_height) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Calculate world position
|
|
793
|
+
let world_x = uniforms.global_min_x + f32(grid_x) * uniforms.resolution;
|
|
794
|
+
let world_y = uniforms.bucket_min_y + f32(grid_y) * uniforms.resolution;
|
|
795
|
+
|
|
796
|
+
// FIND WHICH BUCKET THIS X POSITION BELONGS TO
|
|
797
|
+
// Simple linear search (could be binary search for many buckets)
|
|
798
|
+
var bucket_idx = 0u;
|
|
799
|
+
var found_bucket = false;
|
|
800
|
+
for (var i = 0u; i < uniforms.num_buckets; i++) {
|
|
801
|
+
if (world_x >= all_buckets[i].min_x && world_x < all_buckets[i].max_x) {
|
|
802
|
+
bucket_idx = i;
|
|
803
|
+
found_bucket = true;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// If not in any bucket, write floor and return
|
|
809
|
+
if (!found_bucket) {
|
|
810
|
+
let output_idx = grid_y * uniforms.full_grid_width + grid_x;
|
|
811
|
+
output[output_idx] = uniforms.z_floor;
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let bucket = all_buckets[bucket_idx];
|
|
816
|
+
|
|
817
|
+
// Fixed downward ray from high above
|
|
818
|
+
let ray_origin = vec3<f32>(world_x, world_y, 1000.0);
|
|
819
|
+
|
|
820
|
+
// Track best (closest) hit
|
|
821
|
+
var best_z = uniforms.z_floor;
|
|
822
|
+
|
|
823
|
+
// Test triangles in this bucket with Y-bounds filtering
|
|
824
|
+
for (var i = 0u; i < bucket.count; i++) {
|
|
825
|
+
// Get triangle index from bucket's index array
|
|
826
|
+
let tri_idx = triangle_indices[bucket.start_index + i];
|
|
827
|
+
|
|
828
|
+
// Read Y-bounds first (cheaper than reading all vertices)
|
|
829
|
+
let base = tri_idx * 11u;
|
|
830
|
+
let y_min = rotated_triangles[base + 9u];
|
|
831
|
+
let y_max = rotated_triangles[base + 10u];
|
|
832
|
+
|
|
833
|
+
// Y-bounds check: skip triangles that don't overlap this ray's Y position
|
|
834
|
+
if (y_max < world_y - uniforms.tool_radius ||
|
|
835
|
+
y_min > world_y + uniforms.tool_radius) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Read rotated vertices
|
|
840
|
+
let v0 = vec3<f32>(
|
|
841
|
+
rotated_triangles[base],
|
|
842
|
+
rotated_triangles[base + 1u],
|
|
843
|
+
rotated_triangles[base + 2u]
|
|
844
|
+
);
|
|
845
|
+
let v1 = vec3<f32>(
|
|
846
|
+
rotated_triangles[base + 3u],
|
|
847
|
+
rotated_triangles[base + 4u],
|
|
848
|
+
rotated_triangles[base + 5u]
|
|
849
|
+
);
|
|
850
|
+
let v2 = vec3<f32>(
|
|
851
|
+
rotated_triangles[base + 6u],
|
|
852
|
+
rotated_triangles[base + 7u],
|
|
853
|
+
rotated_triangles[base + 8u]
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
let result = ray_triangle_intersect_downward(ray_origin, v0, v1, v2);
|
|
857
|
+
let hit = result.x;
|
|
858
|
+
let t = result.y;
|
|
859
|
+
|
|
860
|
+
if (hit > 0.5) {
|
|
861
|
+
// Calculate Z position of intersection
|
|
862
|
+
let hit_z = ray_origin.z - t;
|
|
863
|
+
|
|
864
|
+
// Keep highest (max Z) hit
|
|
865
|
+
if (hit_z > best_z) {
|
|
866
|
+
best_z = hit_z;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Write to FULL-WIDTH output (no stitching needed!)
|
|
872
|
+
let output_idx = grid_y * uniforms.full_grid_width + grid_x;
|
|
873
|
+
output[output_idx] = best_z;
|
|
874
|
+
}
|
|
875
|
+
`;
|
|
636
876
|
async function initWebGPU() {
|
|
637
877
|
if (isInitialized)
|
|
638
878
|
return true;
|
|
@@ -682,6 +922,16 @@ async function initWebGPU() {
|
|
|
682
922
|
layout: "auto",
|
|
683
923
|
compute: { module: cachedTracingShaderModule, entryPoint: "main" }
|
|
684
924
|
});
|
|
925
|
+
cachedRadialV3RotateShaderModule = device.createShaderModule({ code: radialV3RotateShaderCode });
|
|
926
|
+
cachedRadialV3RotatePipeline = device.createComputePipeline({
|
|
927
|
+
layout: "auto",
|
|
928
|
+
compute: { module: cachedRadialV3RotateShaderModule, entryPoint: "main" }
|
|
929
|
+
});
|
|
930
|
+
cachedRadialV3BatchedRasterizeShaderModule = device.createShaderModule({ code: radialV3BatchedRasterizeShaderCode });
|
|
931
|
+
cachedRadialV3BatchedRasterizePipeline = device.createComputePipeline({
|
|
932
|
+
layout: "auto",
|
|
933
|
+
compute: { module: cachedRadialV3BatchedRasterizeShaderModule, entryPoint: "main" }
|
|
934
|
+
});
|
|
685
935
|
deviceCapabilities = {
|
|
686
936
|
maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
|
|
687
937
|
maxBufferSize: device.limits.maxBufferSize,
|
|
@@ -2137,7 +2387,7 @@ async function generateRadialToolpaths({
|
|
|
2137
2387
|
if (!strip.positions || strip.positions.length === 0)
|
|
2138
2388
|
continue;
|
|
2139
2389
|
if (diagnostic && (globalStripIdx === 0 || globalStripIdx === 360)) {
|
|
2140
|
-
debug.log(`
|
|
2390
|
+
debug.log(`JRZ5FG9R | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}\xB0) INPUT terrain first 5 Z values: ${strip.positions.slice(0, 5).map((v) => v.toFixed(3)).join(",")}`);
|
|
2141
2391
|
}
|
|
2142
2392
|
const stripToolpathResult = await runToolpathComputeWithBuffers(
|
|
2143
2393
|
strip.positions,
|
|
@@ -2150,7 +2400,7 @@ async function generateRadialToolpaths({
|
|
|
2150
2400
|
pipelineStartTime
|
|
2151
2401
|
);
|
|
2152
2402
|
if (diagnostic && (globalStripIdx === 0 || globalStripIdx === 360)) {
|
|
2153
|
-
debug.log(`
|
|
2403
|
+
debug.log(`JRZ5FG9R | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}\xB0) OUTPUT toolpath first 5 Z values: ${stripToolpathResult.pathData.slice(0, 5).map((v) => v.toFixed(3)).join(",")}`);
|
|
2154
2404
|
}
|
|
2155
2405
|
allStripToolpaths.push({
|
|
2156
2406
|
angle: strip.angle,
|
|
@@ -2200,6 +2450,279 @@ async function generateRadialToolpaths({
|
|
|
2200
2450
|
};
|
|
2201
2451
|
}
|
|
2202
2452
|
|
|
2453
|
+
// src/core/path-radial-v3.js
|
|
2454
|
+
async function rotateTriangles({
|
|
2455
|
+
triangleBuffer,
|
|
2456
|
+
// GPU buffer with original triangles
|
|
2457
|
+
numTriangles,
|
|
2458
|
+
angle
|
|
2459
|
+
// Radians
|
|
2460
|
+
}) {
|
|
2461
|
+
const rotatePipeline = cachedRadialV3RotatePipeline;
|
|
2462
|
+
if (!rotatePipeline) {
|
|
2463
|
+
throw new Error("Radial V3 pipelines not initialized");
|
|
2464
|
+
}
|
|
2465
|
+
const outputSize = numTriangles * 11 * 4;
|
|
2466
|
+
const rotatedBuffer = device.createBuffer({
|
|
2467
|
+
size: outputSize,
|
|
2468
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
2469
|
+
});
|
|
2470
|
+
const uniformBuffer = device.createBuffer({
|
|
2471
|
+
size: 8,
|
|
2472
|
+
// f32 angle + u32 num_triangles
|
|
2473
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2474
|
+
mappedAtCreation: true
|
|
2475
|
+
});
|
|
2476
|
+
const uniformView = new ArrayBuffer(8);
|
|
2477
|
+
const floatView = new Float32Array(uniformView);
|
|
2478
|
+
const uintView = new Uint32Array(uniformView);
|
|
2479
|
+
floatView[0] = angle;
|
|
2480
|
+
uintView[1] = numTriangles;
|
|
2481
|
+
new Uint8Array(uniformBuffer.getMappedRange()).set(new Uint8Array(uniformView));
|
|
2482
|
+
uniformBuffer.unmap();
|
|
2483
|
+
const bindGroup = device.createBindGroup({
|
|
2484
|
+
layout: rotatePipeline.getBindGroupLayout(0),
|
|
2485
|
+
entries: [
|
|
2486
|
+
{ binding: 0, resource: { buffer: triangleBuffer } },
|
|
2487
|
+
{ binding: 1, resource: { buffer: rotatedBuffer } },
|
|
2488
|
+
{ binding: 2, resource: { buffer: uniformBuffer } }
|
|
2489
|
+
]
|
|
2490
|
+
});
|
|
2491
|
+
const commandEncoder = device.createCommandEncoder();
|
|
2492
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
2493
|
+
passEncoder.setPipeline(rotatePipeline);
|
|
2494
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
2495
|
+
passEncoder.dispatchWorkgroups(Math.ceil(numTriangles / 64));
|
|
2496
|
+
passEncoder.end();
|
|
2497
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
2498
|
+
uniformBuffer.destroy();
|
|
2499
|
+
return rotatedBuffer;
|
|
2500
|
+
}
|
|
2501
|
+
async function rasterizeAllBuckets({
|
|
2502
|
+
rotatedTrianglesBuffer,
|
|
2503
|
+
buckets,
|
|
2504
|
+
triangleIndices,
|
|
2505
|
+
resolution,
|
|
2506
|
+
toolRadius,
|
|
2507
|
+
fullGridWidth,
|
|
2508
|
+
gridHeight,
|
|
2509
|
+
globalMinX,
|
|
2510
|
+
bucketMinY,
|
|
2511
|
+
zFloor
|
|
2512
|
+
}) {
|
|
2513
|
+
const rasterizePipeline = cachedRadialV3BatchedRasterizePipeline;
|
|
2514
|
+
if (!rasterizePipeline) {
|
|
2515
|
+
throw new Error("Radial V3 batched pipeline not initialized");
|
|
2516
|
+
}
|
|
2517
|
+
const bucketInfoSize = buckets.length * 16;
|
|
2518
|
+
const bucketInfoBuffer = device.createBuffer({
|
|
2519
|
+
size: bucketInfoSize,
|
|
2520
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
2521
|
+
mappedAtCreation: true
|
|
2522
|
+
});
|
|
2523
|
+
const bucketView = new ArrayBuffer(bucketInfoSize);
|
|
2524
|
+
const bucketFloatView = new Float32Array(bucketView);
|
|
2525
|
+
const bucketUintView = new Uint32Array(bucketView);
|
|
2526
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
2527
|
+
const bucket = buckets[i];
|
|
2528
|
+
const offset = i * 4;
|
|
2529
|
+
bucketFloatView[offset] = bucket.minX;
|
|
2530
|
+
bucketFloatView[offset + 1] = bucket.maxX;
|
|
2531
|
+
bucketUintView[offset + 2] = bucket.startIndex;
|
|
2532
|
+
bucketUintView[offset + 3] = bucket.count;
|
|
2533
|
+
}
|
|
2534
|
+
new Uint8Array(bucketInfoBuffer.getMappedRange()).set(new Uint8Array(bucketView));
|
|
2535
|
+
bucketInfoBuffer.unmap();
|
|
2536
|
+
const indicesBuffer = device.createBuffer({
|
|
2537
|
+
size: triangleIndices.byteLength,
|
|
2538
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
2539
|
+
mappedAtCreation: true
|
|
2540
|
+
});
|
|
2541
|
+
new Uint32Array(indicesBuffer.getMappedRange()).set(triangleIndices);
|
|
2542
|
+
indicesBuffer.unmap();
|
|
2543
|
+
const outputSize = fullGridWidth * gridHeight * 4;
|
|
2544
|
+
const outputBuffer = device.createBuffer({
|
|
2545
|
+
size: outputSize,
|
|
2546
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
|
|
2547
|
+
});
|
|
2548
|
+
const initData = new Float32Array(fullGridWidth * gridHeight);
|
|
2549
|
+
initData.fill(zFloor);
|
|
2550
|
+
device.queue.writeBuffer(outputBuffer, 0, initData);
|
|
2551
|
+
const uniformBuffer = device.createBuffer({
|
|
2552
|
+
size: 32,
|
|
2553
|
+
// 8 fields × 4 bytes
|
|
2554
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2555
|
+
mappedAtCreation: true
|
|
2556
|
+
});
|
|
2557
|
+
const uniformView = new ArrayBuffer(32);
|
|
2558
|
+
const floatView = new Float32Array(uniformView);
|
|
2559
|
+
const uintView = new Uint32Array(uniformView);
|
|
2560
|
+
floatView[0] = resolution;
|
|
2561
|
+
floatView[1] = toolRadius;
|
|
2562
|
+
uintView[2] = fullGridWidth;
|
|
2563
|
+
uintView[3] = gridHeight;
|
|
2564
|
+
floatView[4] = globalMinX;
|
|
2565
|
+
floatView[5] = bucketMinY;
|
|
2566
|
+
floatView[6] = zFloor;
|
|
2567
|
+
uintView[7] = buckets.length;
|
|
2568
|
+
new Uint8Array(uniformBuffer.getMappedRange()).set(new Uint8Array(uniformView));
|
|
2569
|
+
uniformBuffer.unmap();
|
|
2570
|
+
const bindGroup = device.createBindGroup({
|
|
2571
|
+
layout: rasterizePipeline.getBindGroupLayout(0),
|
|
2572
|
+
entries: [
|
|
2573
|
+
{ binding: 0, resource: { buffer: rotatedTrianglesBuffer } },
|
|
2574
|
+
{ binding: 1, resource: { buffer: outputBuffer } },
|
|
2575
|
+
{ binding: 2, resource: { buffer: uniformBuffer } },
|
|
2576
|
+
{ binding: 3, resource: { buffer: bucketInfoBuffer } },
|
|
2577
|
+
{ binding: 4, resource: { buffer: indicesBuffer } }
|
|
2578
|
+
]
|
|
2579
|
+
});
|
|
2580
|
+
const commandEncoder = device.createCommandEncoder();
|
|
2581
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
2582
|
+
passEncoder.setPipeline(rasterizePipeline);
|
|
2583
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
2584
|
+
const dispatchX = Math.ceil(fullGridWidth / 8);
|
|
2585
|
+
const dispatchY = Math.ceil(gridHeight / 8);
|
|
2586
|
+
passEncoder.dispatchWorkgroups(dispatchX, dispatchY);
|
|
2587
|
+
passEncoder.end();
|
|
2588
|
+
const stagingBuffer = device.createBuffer({
|
|
2589
|
+
size: outputSize,
|
|
2590
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
2591
|
+
});
|
|
2592
|
+
commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputSize);
|
|
2593
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
2594
|
+
await device.queue.onSubmittedWorkDone();
|
|
2595
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
2596
|
+
const terrainData = new Float32Array(stagingBuffer.getMappedRange().slice());
|
|
2597
|
+
stagingBuffer.unmap();
|
|
2598
|
+
outputBuffer.destroy();
|
|
2599
|
+
stagingBuffer.destroy();
|
|
2600
|
+
uniformBuffer.destroy();
|
|
2601
|
+
bucketInfoBuffer.destroy();
|
|
2602
|
+
indicesBuffer.destroy();
|
|
2603
|
+
return terrainData;
|
|
2604
|
+
}
|
|
2605
|
+
async function generateRadialToolpathsV3({
|
|
2606
|
+
triangles,
|
|
2607
|
+
bucketData,
|
|
2608
|
+
toolData,
|
|
2609
|
+
resolution,
|
|
2610
|
+
angleStep,
|
|
2611
|
+
numAngles,
|
|
2612
|
+
maxRadius,
|
|
2613
|
+
toolWidth,
|
|
2614
|
+
zFloor,
|
|
2615
|
+
bounds,
|
|
2616
|
+
xStep,
|
|
2617
|
+
yStep
|
|
2618
|
+
}) {
|
|
2619
|
+
debug.log("radial-v3-generate-toolpaths", { triangles: triangles.length / 9, numAngles, resolution });
|
|
2620
|
+
const pipelineStartTime = performance.now();
|
|
2621
|
+
const allStripToolpaths = [];
|
|
2622
|
+
let totalToolpathPoints = 0;
|
|
2623
|
+
const sparseToolData = createSparseToolFromPoints(toolData.positions);
|
|
2624
|
+
debug.log(`Created sparse tool: ${sparseToolData.count} points (reusing for all strips)`);
|
|
2625
|
+
const toolRadius = toolWidth / 2;
|
|
2626
|
+
const bucketMinX = bucketData.buckets[0].minX;
|
|
2627
|
+
const bucketMaxX = bucketData.buckets[bucketData.numBuckets - 1].maxX;
|
|
2628
|
+
const fullWidth = bucketMaxX - bucketMinX;
|
|
2629
|
+
const fullGridWidth = Math.ceil(fullWidth / resolution);
|
|
2630
|
+
const gridHeight = Math.ceil(toolWidth / resolution);
|
|
2631
|
+
debug.log(`Uploading ${triangles.length / 9} triangles to GPU (reused across all angles)...`);
|
|
2632
|
+
const allTrianglesBuffer = device.createBuffer({
|
|
2633
|
+
size: triangles.byteLength,
|
|
2634
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
2635
|
+
mappedAtCreation: true
|
|
2636
|
+
});
|
|
2637
|
+
new Float32Array(allTrianglesBuffer.getMappedRange()).set(triangles);
|
|
2638
|
+
allTrianglesBuffer.unmap();
|
|
2639
|
+
for (let angleIdx = 0; angleIdx < numAngles; angleIdx++) {
|
|
2640
|
+
const angle = -(angleIdx * angleStep * (Math.PI / 180));
|
|
2641
|
+
const angleDegrees = angleIdx * angleStep;
|
|
2642
|
+
if (diagnostic) {
|
|
2643
|
+
debug.log(`Angle ${angleIdx + 1}/${numAngles}: ${angleDegrees.toFixed(1)}\xB0`);
|
|
2644
|
+
}
|
|
2645
|
+
if (angleIdx % 10 === 0 || angleIdx === numAngles - 1) {
|
|
2646
|
+
const stripProgress = (angleIdx + 1) / numAngles * 98;
|
|
2647
|
+
self.postMessage({
|
|
2648
|
+
type: "toolpath-progress",
|
|
2649
|
+
data: {
|
|
2650
|
+
percent: Math.round(stripProgress),
|
|
2651
|
+
current: angleIdx + 1,
|
|
2652
|
+
total: numAngles,
|
|
2653
|
+
layer: angleIdx + 1
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
const numTotalTriangles = triangles.length / 9;
|
|
2658
|
+
const allRotatedTrianglesBuffer = await rotateTriangles({
|
|
2659
|
+
triangleBuffer: allTrianglesBuffer,
|
|
2660
|
+
numTriangles: numTotalTriangles,
|
|
2661
|
+
angle
|
|
2662
|
+
});
|
|
2663
|
+
const fullTerrainStrip = await rasterizeAllBuckets({
|
|
2664
|
+
rotatedTrianglesBuffer: allRotatedTrianglesBuffer,
|
|
2665
|
+
buckets: bucketData.buckets,
|
|
2666
|
+
triangleIndices: bucketData.triangleIndices,
|
|
2667
|
+
resolution,
|
|
2668
|
+
toolRadius,
|
|
2669
|
+
fullGridWidth,
|
|
2670
|
+
gridHeight,
|
|
2671
|
+
globalMinX: bucketMinX,
|
|
2672
|
+
bucketMinY: -toolWidth / 2,
|
|
2673
|
+
zFloor
|
|
2674
|
+
});
|
|
2675
|
+
allRotatedTrianglesBuffer.destroy();
|
|
2676
|
+
const reusableToolpathBuffers = createReusableToolpathBuffers(
|
|
2677
|
+
fullGridWidth,
|
|
2678
|
+
gridHeight,
|
|
2679
|
+
sparseToolData,
|
|
2680
|
+
xStep,
|
|
2681
|
+
gridHeight
|
|
2682
|
+
);
|
|
2683
|
+
const stripToolpathResult = await runToolpathComputeWithBuffers(
|
|
2684
|
+
fullTerrainStrip,
|
|
2685
|
+
fullGridWidth,
|
|
2686
|
+
gridHeight,
|
|
2687
|
+
xStep,
|
|
2688
|
+
gridHeight,
|
|
2689
|
+
zFloor,
|
|
2690
|
+
reusableToolpathBuffers,
|
|
2691
|
+
pipelineStartTime
|
|
2692
|
+
);
|
|
2693
|
+
destroyReusableToolpathBuffers(reusableToolpathBuffers);
|
|
2694
|
+
allStripToolpaths.push({
|
|
2695
|
+
angle: angleDegrees,
|
|
2696
|
+
pathData: stripToolpathResult.pathData,
|
|
2697
|
+
numScanlines: stripToolpathResult.numScanlines,
|
|
2698
|
+
pointsPerLine: stripToolpathResult.pointsPerLine,
|
|
2699
|
+
terrainBounds: {
|
|
2700
|
+
min: { x: bucketMinX, y: -toolWidth / 2, z: zFloor },
|
|
2701
|
+
max: { x: bucketMaxX, y: toolWidth / 2, z: bounds.max.z }
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
totalToolpathPoints += stripToolpathResult.pathData.length;
|
|
2705
|
+
}
|
|
2706
|
+
allTrianglesBuffer.destroy();
|
|
2707
|
+
debug.log(`Destroyed reusable triangle buffer`);
|
|
2708
|
+
const pipelineTotalTime = performance.now() - pipelineStartTime;
|
|
2709
|
+
debug.log(`Complete radial V3 toolpath: ${allStripToolpaths.length} strips, ${totalToolpathPoints} total points in ${pipelineTotalTime.toFixed(0)}ms`);
|
|
2710
|
+
self.postMessage({
|
|
2711
|
+
type: "toolpath-progress",
|
|
2712
|
+
data: {
|
|
2713
|
+
percent: 100,
|
|
2714
|
+
current: bucketData.numBuckets * numAngles,
|
|
2715
|
+
total: bucketData.numBuckets * numAngles,
|
|
2716
|
+
layer: numAngles
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
return {
|
|
2720
|
+
strips: allStripToolpaths,
|
|
2721
|
+
totalPoints: totalToolpathPoints,
|
|
2722
|
+
numStrips: allStripToolpaths.length
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2203
2726
|
// src/core/path-tracing.js
|
|
2204
2727
|
var cachedTracingBuffers = null;
|
|
2205
2728
|
function createReusableTracingBuffers(terrainPositions, toolPositions) {
|
|
@@ -2803,6 +3326,14 @@ self.onmessage = async function(e) {
|
|
|
2803
3326
|
data: radialToolpathResult
|
|
2804
3327
|
}, toolpathTransferBuffers);
|
|
2805
3328
|
break;
|
|
3329
|
+
case "radial-generate-toolpaths-v3":
|
|
3330
|
+
const radialV3ToolpathResult = await generateRadialToolpathsV3(data);
|
|
3331
|
+
const v3ToolpathTransferBuffers = radialV3ToolpathResult.strips.map((strip) => strip.pathData.buffer);
|
|
3332
|
+
self.postMessage({
|
|
3333
|
+
type: "radial-toolpaths-complete",
|
|
3334
|
+
data: radialV3ToolpathResult
|
|
3335
|
+
}, v3ToolpathTransferBuffers);
|
|
3336
|
+
break;
|
|
2806
3337
|
case "tracing-generate-toolpaths":
|
|
2807
3338
|
const tracingResult = await generateTracingToolpaths({
|
|
2808
3339
|
paths: data.paths,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gridspace/raster-path",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Terrain and Tool Raster Path Finder using WebGPU",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"build:web": "mkdir -p build && cp src/web/*.html src/web/*.css src/web/*.js build/ && cp src/core/raster-path.js build/raster-path.js && cp src/etc/serve.json build/",
|
|
33
33
|
"clean": "rm -rf build/",
|
|
34
34
|
"dev": "npm run build && npm run serve",
|
|
35
|
-
"serve": "npx serve build --config serve.json --listen 9090",
|
|
35
|
+
"serve": "npm run build && npx serve build --config serve.json --listen 9090",
|
|
36
36
|
"publish:public": "npm publish --access public",
|
|
37
37
|
"test": "npm run test:planar && npm run test:radial",
|
|
38
38
|
"test:planar": "npm run build && npx electron src/test/planar-test.cjs",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"test:lathe-cylinder-2-debug": "npm run build && npx electron src/test/lathe-cylinder-2-debug.cjs",
|
|
46
46
|
"test:extreme-work": "npm run build && npx electron src/test/extreme-work-test.cjs",
|
|
47
47
|
"test:radial-thread-limit": "npm run build && npx electron src/test/radial-thread-limit-test.cjs",
|
|
48
|
+
"test:radial-v3": "npm run build && npx electron src/test/radial-v3-benchmark.cjs",
|
|
48
49
|
"test:tracing": "npm run build && npx electron src/test/tracing-test.cjs"
|
|
49
50
|
},
|
|
50
51
|
"keywords": [
|