@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/build/raster-worker.js
CHANGED
|
@@ -9,6 +9,12 @@ var cachedToolpathPipeline = null;
|
|
|
9
9
|
var cachedToolpathShaderModule = null;
|
|
10
10
|
var cachedRadialBatchPipeline = null;
|
|
11
11
|
var cachedRadialBatchShaderModule = null;
|
|
12
|
+
var cachedTracingPipeline = null;
|
|
13
|
+
var cachedTracingShaderModule = null;
|
|
14
|
+
var cachedRadialV3RotatePipeline = null;
|
|
15
|
+
var cachedRadialV3RotateShaderModule = null;
|
|
16
|
+
var cachedRadialV3BatchedRasterizePipeline = null;
|
|
17
|
+
var cachedRadialV3BatchedRasterizeShaderModule = null;
|
|
12
18
|
var EMPTY_CELL = -1e10;
|
|
13
19
|
var log_pre = "[Worker]";
|
|
14
20
|
var diagnostic = false;
|
|
@@ -535,6 +541,338 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
|
535
541
|
}
|
|
536
542
|
}
|
|
537
543
|
`;
|
|
544
|
+
var tracingShaderCode = `// Tracing toolpath generation
|
|
545
|
+
// Follows input polylines and calculates Z-depth at each sampled point
|
|
546
|
+
// Sentinel value for empty terrain cells (must match rasterize shader)
|
|
547
|
+
const EMPTY_CELL: f32 = -1e10;
|
|
548
|
+
const MAX_F32: f32 = 3.402823466e+38;
|
|
549
|
+
|
|
550
|
+
struct SparseToolPoint {
|
|
551
|
+
x_offset: i32,
|
|
552
|
+
y_offset: i32,
|
|
553
|
+
z_value: f32,
|
|
554
|
+
padding: f32,
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
struct Uniforms {
|
|
558
|
+
terrain_width: u32,
|
|
559
|
+
terrain_height: u32,
|
|
560
|
+
tool_count: u32,
|
|
561
|
+
point_count: u32, // Number of sampled points to process
|
|
562
|
+
path_index: u32, // Index of current path being processed
|
|
563
|
+
terrain_min_x: f32, // Terrain bounding box (world coordinates)
|
|
564
|
+
terrain_min_y: f32,
|
|
565
|
+
grid_step: f32, // Resolution of terrain rasterization
|
|
566
|
+
oob_z: f32, // Z value for out-of-bounds points (zFloor)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
@group(0) @binding(0) var<storage, read> terrain_map: array<f32>;
|
|
570
|
+
@group(0) @binding(1) var<storage, read> sparse_tool: array<SparseToolPoint>;
|
|
571
|
+
@group(0) @binding(2) var<storage, read> input_points: array<f32>; // XY pairs
|
|
572
|
+
@group(0) @binding(3) var<storage, read_write> output_depths: array<f32>; // Z values
|
|
573
|
+
@group(0) @binding(4) var<storage, read_write> max_z_buffer: array<atomic<i32>>; // Max Z per path (as bits)
|
|
574
|
+
@group(0) @binding(5) var<uniform> uniforms: Uniforms;
|
|
575
|
+
|
|
576
|
+
@compute @workgroup_size(64, 1, 1)
|
|
577
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
578
|
+
let point_idx = global_id.x;
|
|
579
|
+
|
|
580
|
+
if (point_idx >= uniforms.point_count) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Read input X,Y world coordinates
|
|
585
|
+
let world_x = input_points[point_idx * 2u + 0u];
|
|
586
|
+
let world_y = input_points[point_idx * 2u + 1u];
|
|
587
|
+
|
|
588
|
+
// Convert world coordinates to grid coordinates
|
|
589
|
+
let grid_x_f32 = (world_x - uniforms.terrain_min_x) / uniforms.grid_step;
|
|
590
|
+
let grid_y_f32 = (world_y - uniforms.terrain_min_y) / uniforms.grid_step;
|
|
591
|
+
let tool_center_x = i32(grid_x_f32);
|
|
592
|
+
let tool_center_y = i32(grid_y_f32);
|
|
593
|
+
|
|
594
|
+
// Check if tool center is outside terrain bounds
|
|
595
|
+
let center_oob = tool_center_x < 0 || tool_center_x >= i32(uniforms.terrain_width) ||
|
|
596
|
+
tool_center_y < 0 || tool_center_y >= i32(uniforms.terrain_height);
|
|
597
|
+
|
|
598
|
+
var max_collision_z = uniforms.oob_z;
|
|
599
|
+
var found_collision = false;
|
|
600
|
+
|
|
601
|
+
// Test each tool point for collision with terrain
|
|
602
|
+
for (var i = 0u; i < uniforms.tool_count; i++) {
|
|
603
|
+
let tool_point = sparse_tool[i];
|
|
604
|
+
let terrain_x = tool_center_x + tool_point.x_offset;
|
|
605
|
+
let terrain_y = tool_center_y + tool_point.y_offset;
|
|
606
|
+
|
|
607
|
+
// Bounds check: terrain sample must be within terrain grid
|
|
608
|
+
if (terrain_x < 0 || terrain_x >= i32(uniforms.terrain_width) ||
|
|
609
|
+
terrain_y < 0 || terrain_y >= i32(uniforms.terrain_height)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let terrain_idx = u32(terrain_y) * uniforms.terrain_width + u32(terrain_x);
|
|
614
|
+
let terrain_z = terrain_map[terrain_idx];
|
|
615
|
+
|
|
616
|
+
// Check if terrain cell has geometry (not empty sentinel value)
|
|
617
|
+
if (terrain_z > EMPTY_CELL + 1.0) {
|
|
618
|
+
// Tool z_value is positive offset from tip (tip=0, shaft=+50)
|
|
619
|
+
// Add to terrain height to find where tool center needs to be
|
|
620
|
+
let collision_z = terrain_z + tool_point.z_value;
|
|
621
|
+
max_collision_z = max(max_collision_z, collision_z);
|
|
622
|
+
found_collision = true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// If no collision found and center was in-bounds, use oob_z
|
|
627
|
+
var output_z = uniforms.oob_z;
|
|
628
|
+
if (found_collision) {
|
|
629
|
+
output_z = max_collision_z;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
output_depths[point_idx] = output_z;
|
|
633
|
+
|
|
634
|
+
// Update max Z for this path using atomic operation
|
|
635
|
+
// Convert float to int bits for atomic comparison
|
|
636
|
+
let z_bits = bitcast<i32>(output_z);
|
|
637
|
+
atomicMax(&max_z_buffer[uniforms.path_index], z_bits);
|
|
638
|
+
}
|
|
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
|
+
`;
|
|
538
876
|
async function initWebGPU() {
|
|
539
877
|
if (isInitialized)
|
|
540
878
|
return true;
|
|
@@ -549,10 +887,7 @@ async function initWebGPU() {
|
|
|
549
887
|
return false;
|
|
550
888
|
}
|
|
551
889
|
const adapterLimits = adapter.limits;
|
|
552
|
-
debug.log("Adapter limits:",
|
|
553
|
-
maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
|
|
554
|
-
maxBufferSize: adapterLimits.maxBufferSize
|
|
555
|
-
});
|
|
890
|
+
debug.log("Adapter limits:", adapterLimits);
|
|
556
891
|
device = await adapter.requestDevice({
|
|
557
892
|
requiredLimits: {
|
|
558
893
|
maxStorageBufferBindingSize: Math.min(
|
|
@@ -582,6 +917,21 @@ async function initWebGPU() {
|
|
|
582
917
|
layout: "auto",
|
|
583
918
|
compute: { module: cachedRadialBatchShaderModule, entryPoint: "main" }
|
|
584
919
|
});
|
|
920
|
+
cachedTracingShaderModule = device.createShaderModule({ code: tracingShaderCode });
|
|
921
|
+
cachedTracingPipeline = device.createComputePipeline({
|
|
922
|
+
layout: "auto",
|
|
923
|
+
compute: { module: cachedTracingShaderModule, entryPoint: "main" }
|
|
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
|
+
});
|
|
585
935
|
deviceCapabilities = {
|
|
586
936
|
maxStorageBufferBindingSize: device.limits.maxStorageBufferBindingSize,
|
|
587
937
|
maxBufferSize: device.limits.maxBufferSize,
|
|
@@ -2037,7 +2387,7 @@ async function generateRadialToolpaths({
|
|
|
2037
2387
|
if (!strip.positions || strip.positions.length === 0)
|
|
2038
2388
|
continue;
|
|
2039
2389
|
if (diagnostic && (globalStripIdx === 0 || globalStripIdx === 360)) {
|
|
2040
|
-
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(",")}`);
|
|
2041
2391
|
}
|
|
2042
2392
|
const stripToolpathResult = await runToolpathComputeWithBuffers(
|
|
2043
2393
|
strip.positions,
|
|
@@ -2050,7 +2400,7 @@ async function generateRadialToolpaths({
|
|
|
2050
2400
|
pipelineStartTime
|
|
2051
2401
|
);
|
|
2052
2402
|
if (diagnostic && (globalStripIdx === 0 || globalStripIdx === 360)) {
|
|
2053
|
-
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(",")}`);
|
|
2054
2404
|
}
|
|
2055
2405
|
allStripToolpaths.push({
|
|
2056
2406
|
angle: strip.angle,
|
|
@@ -2069,7 +2419,6 @@ async function generateRadialToolpaths({
|
|
|
2069
2419
|
const batchTotalTime = performance.now() - batchStartTime;
|
|
2070
2420
|
Object.assign(batchInfo, {
|
|
2071
2421
|
"prep": batchInfo.prep || 0,
|
|
2072
|
-
"gpu": batchInfo.gpu || 0,
|
|
2073
2422
|
"stitch": batchInfo.stitch || 0,
|
|
2074
2423
|
"raster": batchInfo.raster || 0,
|
|
2075
2424
|
"paths": toolpathTime | 0,
|
|
@@ -2101,6 +2450,557 @@ async function generateRadialToolpaths({
|
|
|
2101
2450
|
};
|
|
2102
2451
|
}
|
|
2103
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
|
+
|
|
2726
|
+
// src/core/path-tracing.js
|
|
2727
|
+
var cachedTracingBuffers = null;
|
|
2728
|
+
function createReusableTracingBuffers(terrainPositions, toolPositions) {
|
|
2729
|
+
if (!isInitialized) {
|
|
2730
|
+
throw new Error("WebGPU not initialized");
|
|
2731
|
+
}
|
|
2732
|
+
if (cachedTracingBuffers) {
|
|
2733
|
+
destroyReusableTracingBuffers();
|
|
2734
|
+
}
|
|
2735
|
+
const sparseToolData = createSparseToolFromPoints(toolPositions);
|
|
2736
|
+
debug.log(`Created reusable tracing buffers: terrain ${terrainPositions.length} floats, tool ${sparseToolData.count} points`);
|
|
2737
|
+
const terrainBuffer = device.createBuffer({
|
|
2738
|
+
size: terrainPositions.byteLength,
|
|
2739
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
2740
|
+
});
|
|
2741
|
+
device.queue.writeBuffer(terrainBuffer, 0, terrainPositions);
|
|
2742
|
+
const toolBufferData = new ArrayBuffer(sparseToolData.count * 16);
|
|
2743
|
+
const toolBufferI32 = new Int32Array(toolBufferData);
|
|
2744
|
+
const toolBufferF32 = new Float32Array(toolBufferData);
|
|
2745
|
+
for (let i = 0; i < sparseToolData.count; i++) {
|
|
2746
|
+
toolBufferI32[i * 4 + 0] = sparseToolData.xOffsets[i];
|
|
2747
|
+
toolBufferI32[i * 4 + 1] = sparseToolData.yOffsets[i];
|
|
2748
|
+
toolBufferF32[i * 4 + 2] = sparseToolData.zValues[i];
|
|
2749
|
+
toolBufferF32[i * 4 + 3] = 0;
|
|
2750
|
+
}
|
|
2751
|
+
const toolBuffer = device.createBuffer({
|
|
2752
|
+
size: toolBufferData.byteLength,
|
|
2753
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
2754
|
+
});
|
|
2755
|
+
device.queue.writeBuffer(toolBuffer, 0, toolBufferData);
|
|
2756
|
+
cachedTracingBuffers = {
|
|
2757
|
+
terrainBuffer,
|
|
2758
|
+
toolBuffer,
|
|
2759
|
+
sparseToolData
|
|
2760
|
+
};
|
|
2761
|
+
return cachedTracingBuffers;
|
|
2762
|
+
}
|
|
2763
|
+
function destroyReusableTracingBuffers() {
|
|
2764
|
+
if (cachedTracingBuffers) {
|
|
2765
|
+
cachedTracingBuffers.terrainBuffer.destroy();
|
|
2766
|
+
cachedTracingBuffers.toolBuffer.destroy();
|
|
2767
|
+
cachedTracingBuffers = null;
|
|
2768
|
+
debug.log("Destroyed reusable tracing buffers");
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
function samplePath(pathXY, step) {
|
|
2772
|
+
if (pathXY.length < 2) {
|
|
2773
|
+
return new Float32Array(pathXY);
|
|
2774
|
+
}
|
|
2775
|
+
const numVertices = pathXY.length / 2;
|
|
2776
|
+
const sampledPoints = [];
|
|
2777
|
+
sampledPoints.push(pathXY[0], pathXY[1]);
|
|
2778
|
+
for (let i = 0; i < numVertices - 1; i++) {
|
|
2779
|
+
const x1 = pathXY[i * 2];
|
|
2780
|
+
const y1 = pathXY[i * 2 + 1];
|
|
2781
|
+
const x2 = pathXY[(i + 1) * 2];
|
|
2782
|
+
const y2 = pathXY[(i + 1) * 2 + 1];
|
|
2783
|
+
const dx = x2 - x1;
|
|
2784
|
+
const dy = y2 - y1;
|
|
2785
|
+
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
2786
|
+
if (segmentLength > step) {
|
|
2787
|
+
const numSubdivisions = Math.ceil(segmentLength / step);
|
|
2788
|
+
const subdivisionStep = 1 / numSubdivisions;
|
|
2789
|
+
for (let j = 1; j < numSubdivisions; j++) {
|
|
2790
|
+
const t = j * subdivisionStep;
|
|
2791
|
+
const x = x1 + t * dx;
|
|
2792
|
+
const y = y1 + t * dy;
|
|
2793
|
+
sampledPoints.push(x, y);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (i < numVertices - 1) {
|
|
2797
|
+
sampledPoints.push(x2, y2);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return new Float32Array(sampledPoints);
|
|
2801
|
+
}
|
|
2802
|
+
async function generateTracingToolpaths({
|
|
2803
|
+
paths,
|
|
2804
|
+
terrainPositions,
|
|
2805
|
+
terrainData,
|
|
2806
|
+
toolPositions,
|
|
2807
|
+
step,
|
|
2808
|
+
gridStep,
|
|
2809
|
+
terrainBounds,
|
|
2810
|
+
zFloor,
|
|
2811
|
+
onProgress
|
|
2812
|
+
}) {
|
|
2813
|
+
const startTime = performance.now();
|
|
2814
|
+
debug.log("Generating tracing toolpaths...");
|
|
2815
|
+
debug.log(`Input: ${paths.length} paths, step=${step}, gridStep=${gridStep}, zFloor=${zFloor}`);
|
|
2816
|
+
debug.log(`Terrain: ${terrainData.width}\xD7${terrainData.height}, bounds: min(${terrainBounds.min.x.toFixed(2)}, ${terrainBounds.min.y.toFixed(2)}) max(${terrainBounds.max.x.toFixed(2)}, ${terrainBounds.max.y.toFixed(2)})`);
|
|
2817
|
+
if (!isInitialized) {
|
|
2818
|
+
const success = await initWebGPU();
|
|
2819
|
+
if (!success) {
|
|
2820
|
+
throw new Error("WebGPU not available");
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
let terrainBuffer, toolBuffer, sparseToolData;
|
|
2824
|
+
let shouldCleanupBuffers = false;
|
|
2825
|
+
if (cachedTracingBuffers) {
|
|
2826
|
+
debug.log("Using cached tracing buffers");
|
|
2827
|
+
terrainBuffer = cachedTracingBuffers.terrainBuffer;
|
|
2828
|
+
toolBuffer = cachedTracingBuffers.toolBuffer;
|
|
2829
|
+
sparseToolData = cachedTracingBuffers.sparseToolData;
|
|
2830
|
+
} else {
|
|
2831
|
+
debug.log("Creating temporary tracing buffers");
|
|
2832
|
+
sparseToolData = createSparseToolFromPoints(toolPositions);
|
|
2833
|
+
debug.log(`Created sparse tool: ${sparseToolData.count} points`);
|
|
2834
|
+
terrainBuffer = device.createBuffer({
|
|
2835
|
+
size: terrainPositions.byteLength,
|
|
2836
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
2837
|
+
});
|
|
2838
|
+
device.queue.writeBuffer(terrainBuffer, 0, terrainPositions);
|
|
2839
|
+
const toolBufferData = new ArrayBuffer(sparseToolData.count * 16);
|
|
2840
|
+
const toolBufferI32 = new Int32Array(toolBufferData);
|
|
2841
|
+
const toolBufferF32 = new Float32Array(toolBufferData);
|
|
2842
|
+
for (let i = 0; i < sparseToolData.count; i++) {
|
|
2843
|
+
toolBufferI32[i * 4 + 0] = sparseToolData.xOffsets[i];
|
|
2844
|
+
toolBufferI32[i * 4 + 1] = sparseToolData.yOffsets[i];
|
|
2845
|
+
toolBufferF32[i * 4 + 2] = sparseToolData.zValues[i];
|
|
2846
|
+
toolBufferF32[i * 4 + 3] = 0;
|
|
2847
|
+
}
|
|
2848
|
+
toolBuffer = device.createBuffer({
|
|
2849
|
+
size: toolBufferData.byteLength,
|
|
2850
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
2851
|
+
});
|
|
2852
|
+
device.queue.writeBuffer(toolBuffer, 0, toolBufferData);
|
|
2853
|
+
await device.queue.onSubmittedWorkDone();
|
|
2854
|
+
shouldCleanupBuffers = true;
|
|
2855
|
+
}
|
|
2856
|
+
const SENTINEL_Z = -1e30;
|
|
2857
|
+
const sentinelBits = new Float32Array([SENTINEL_Z]);
|
|
2858
|
+
const sentinelI32 = new Int32Array(sentinelBits.buffer)[0];
|
|
2859
|
+
const maxZInitData = new Int32Array(paths.length).fill(sentinelI32);
|
|
2860
|
+
const maxZBuffer = device.createBuffer({
|
|
2861
|
+
size: maxZInitData.byteLength,
|
|
2862
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
2863
|
+
});
|
|
2864
|
+
device.queue.writeBuffer(maxZBuffer, 0, maxZInitData);
|
|
2865
|
+
const outputPaths = [];
|
|
2866
|
+
let totalSampledPoints = 0;
|
|
2867
|
+
for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
|
|
2868
|
+
const pathStartTime = performance.now();
|
|
2869
|
+
const inputPath = paths[pathIdx];
|
|
2870
|
+
debug.log(`Processing path ${pathIdx + 1}/${paths.length}: ${inputPath.length / 2} input vertices`);
|
|
2871
|
+
const sampledPath = samplePath(inputPath, step);
|
|
2872
|
+
const numSampledPoints = sampledPath.length / 2;
|
|
2873
|
+
totalSampledPoints += numSampledPoints;
|
|
2874
|
+
debug.log(` Sampled to ${numSampledPoints} points`);
|
|
2875
|
+
const firstX = sampledPath[0];
|
|
2876
|
+
const firstY = sampledPath[1];
|
|
2877
|
+
const gridX = (firstX - terrainBounds.min.x) / gridStep;
|
|
2878
|
+
const gridY = (firstY - terrainBounds.min.y) / gridStep;
|
|
2879
|
+
debug.log(` First point: world(${firstX.toFixed(2)}, ${firstY.toFixed(2)}) -> grid(${gridX.toFixed(2)}, ${gridY.toFixed(2)})`);
|
|
2880
|
+
debug.log(` Terrain: ${terrainData.width}x${terrainData.height}, bounds: (${terrainBounds.min.x.toFixed(2)}, ${terrainBounds.min.y.toFixed(2)}) to (${terrainBounds.max.x.toFixed(2)}, ${terrainBounds.max.y.toFixed(2)})`);
|
|
2881
|
+
const inputBufferSize = sampledPath.byteLength;
|
|
2882
|
+
const outputBufferSize = numSampledPoints * 4;
|
|
2883
|
+
const estimatedMemory = inputBufferSize + outputBufferSize;
|
|
2884
|
+
const configuredLimit = config.maxGPUMemoryMB * 1024 * 1024;
|
|
2885
|
+
const deviceLimit = deviceCapabilities.maxStorageBufferBindingSize;
|
|
2886
|
+
const maxSafeSize = Math.min(configuredLimit, deviceLimit) * config.gpuMemorySafetyMargin;
|
|
2887
|
+
if (estimatedMemory > maxSafeSize) {
|
|
2888
|
+
terrainBuffer.destroy();
|
|
2889
|
+
toolBuffer.destroy();
|
|
2890
|
+
throw new Error(
|
|
2891
|
+
`Path ${pathIdx + 1} exceeds GPU memory limits: ${(estimatedMemory / 1024 / 1024).toFixed(1)}MB > ${(maxSafeSize / 1024 / 1024).toFixed(1)}MB safe limit. Consider reducing step parameter or splitting path.`
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
const inputBuffer = device.createBuffer({
|
|
2895
|
+
size: sampledPath.byteLength,
|
|
2896
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
2897
|
+
});
|
|
2898
|
+
device.queue.writeBuffer(inputBuffer, 0, sampledPath);
|
|
2899
|
+
const outputBuffer = device.createBuffer({
|
|
2900
|
+
size: outputBufferSize,
|
|
2901
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
2902
|
+
});
|
|
2903
|
+
const uniformData = new Uint32Array(12);
|
|
2904
|
+
uniformData[0] = terrainData.width;
|
|
2905
|
+
uniformData[1] = terrainData.height;
|
|
2906
|
+
uniformData[2] = sparseToolData.count;
|
|
2907
|
+
uniformData[3] = numSampledPoints;
|
|
2908
|
+
uniformData[4] = pathIdx;
|
|
2909
|
+
const uniformDataFloat = new Float32Array(uniformData.buffer);
|
|
2910
|
+
uniformDataFloat[5] = terrainBounds.min.x;
|
|
2911
|
+
uniformDataFloat[6] = terrainBounds.min.y;
|
|
2912
|
+
uniformDataFloat[7] = gridStep;
|
|
2913
|
+
uniformDataFloat[8] = zFloor;
|
|
2914
|
+
const uniformBuffer = device.createBuffer({
|
|
2915
|
+
size: uniformData.byteLength,
|
|
2916
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
2917
|
+
});
|
|
2918
|
+
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
|
|
2919
|
+
await device.queue.onSubmittedWorkDone();
|
|
2920
|
+
const bindGroup = device.createBindGroup({
|
|
2921
|
+
layout: cachedTracingPipeline.getBindGroupLayout(0),
|
|
2922
|
+
entries: [
|
|
2923
|
+
{ binding: 0, resource: { buffer: terrainBuffer } },
|
|
2924
|
+
{ binding: 1, resource: { buffer: toolBuffer } },
|
|
2925
|
+
{ binding: 2, resource: { buffer: inputBuffer } },
|
|
2926
|
+
{ binding: 3, resource: { buffer: outputBuffer } },
|
|
2927
|
+
{ binding: 4, resource: { buffer: maxZBuffer } },
|
|
2928
|
+
{ binding: 5, resource: { buffer: uniformBuffer } }
|
|
2929
|
+
]
|
|
2930
|
+
});
|
|
2931
|
+
const commandEncoder = device.createCommandEncoder();
|
|
2932
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
2933
|
+
passEncoder.setPipeline(cachedTracingPipeline);
|
|
2934
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
2935
|
+
const workgroupsX = Math.ceil(numSampledPoints / 64);
|
|
2936
|
+
passEncoder.dispatchWorkgroups(workgroupsX);
|
|
2937
|
+
passEncoder.end();
|
|
2938
|
+
const stagingBuffer = device.createBuffer({
|
|
2939
|
+
size: outputBufferSize,
|
|
2940
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
2941
|
+
});
|
|
2942
|
+
commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputBufferSize);
|
|
2943
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
2944
|
+
await device.queue.onSubmittedWorkDone();
|
|
2945
|
+
await stagingBuffer.mapAsync(GPUMapMode.READ);
|
|
2946
|
+
const outputDepths = new Float32Array(stagingBuffer.getMappedRange());
|
|
2947
|
+
const depthsCopy = new Float32Array(outputDepths);
|
|
2948
|
+
stagingBuffer.unmap();
|
|
2949
|
+
const outputXYZ = new Float32Array(numSampledPoints * 3);
|
|
2950
|
+
for (let i = 0; i < numSampledPoints; i++) {
|
|
2951
|
+
outputXYZ[i * 3 + 0] = sampledPath[i * 2 + 0];
|
|
2952
|
+
outputXYZ[i * 3 + 1] = sampledPath[i * 2 + 1];
|
|
2953
|
+
outputXYZ[i * 3 + 2] = depthsCopy[i];
|
|
2954
|
+
}
|
|
2955
|
+
outputPaths.push(outputXYZ);
|
|
2956
|
+
inputBuffer.destroy();
|
|
2957
|
+
outputBuffer.destroy();
|
|
2958
|
+
uniformBuffer.destroy();
|
|
2959
|
+
stagingBuffer.destroy();
|
|
2960
|
+
const pathTime = performance.now() - pathStartTime;
|
|
2961
|
+
debug.log(` Path ${pathIdx + 1} complete: ${numSampledPoints} points in ${pathTime.toFixed(1)}ms`);
|
|
2962
|
+
if (onProgress) {
|
|
2963
|
+
onProgress({
|
|
2964
|
+
type: "tracing-progress",
|
|
2965
|
+
data: {
|
|
2966
|
+
percent: Math.round((pathIdx + 1) / paths.length * 100),
|
|
2967
|
+
current: pathIdx + 1,
|
|
2968
|
+
total: paths.length,
|
|
2969
|
+
pathIndex: pathIdx
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
const maxZStagingBuffer = device.createBuffer({
|
|
2975
|
+
size: maxZInitData.byteLength,
|
|
2976
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
2977
|
+
});
|
|
2978
|
+
const maxZCommandEncoder = device.createCommandEncoder();
|
|
2979
|
+
maxZCommandEncoder.copyBufferToBuffer(maxZBuffer, 0, maxZStagingBuffer, 0, maxZInitData.byteLength);
|
|
2980
|
+
device.queue.submit([maxZCommandEncoder.finish()]);
|
|
2981
|
+
await device.queue.onSubmittedWorkDone();
|
|
2982
|
+
await maxZStagingBuffer.mapAsync(GPUMapMode.READ);
|
|
2983
|
+
const maxZBitsI32 = new Int32Array(maxZStagingBuffer.getMappedRange());
|
|
2984
|
+
const maxZBitsCopy = new Int32Array(maxZBitsI32);
|
|
2985
|
+
maxZStagingBuffer.unmap();
|
|
2986
|
+
const maxZValues = new Float32Array(maxZBitsCopy.buffer);
|
|
2987
|
+
maxZBuffer.destroy();
|
|
2988
|
+
maxZStagingBuffer.destroy();
|
|
2989
|
+
if (shouldCleanupBuffers) {
|
|
2990
|
+
terrainBuffer.destroy();
|
|
2991
|
+
toolBuffer.destroy();
|
|
2992
|
+
debug.log("Cleaned up temporary tracing buffers");
|
|
2993
|
+
}
|
|
2994
|
+
const endTime = performance.now();
|
|
2995
|
+
debug.log(`Tracing complete: ${paths.length} paths, ${totalSampledPoints} total points in ${(endTime - startTime).toFixed(1)}ms`);
|
|
2996
|
+
debug.log(`Max Z values: [${Array.from(maxZValues).map((z) => z.toFixed(2)).join(", ")}]`);
|
|
2997
|
+
return {
|
|
2998
|
+
paths: outputPaths,
|
|
2999
|
+
maxZ: Array.from(maxZValues),
|
|
3000
|
+
generationTime: endTime - startTime
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
|
|
2104
3004
|
// src/core/workload-calibrate.js
|
|
2105
3005
|
var calibrateShaderCode = `// Workload Calibration Shader
|
|
2106
3006
|
// Tests GPU watchdog limits by doing configurable amount of work per thread
|
|
@@ -2426,6 +3326,51 @@ self.onmessage = async function(e) {
|
|
|
2426
3326
|
data: radialToolpathResult
|
|
2427
3327
|
}, toolpathTransferBuffers);
|
|
2428
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;
|
|
3337
|
+
case "tracing-generate-toolpaths":
|
|
3338
|
+
const tracingResult = await generateTracingToolpaths({
|
|
3339
|
+
paths: data.paths,
|
|
3340
|
+
terrainPositions: data.terrainPositions,
|
|
3341
|
+
terrainData: data.terrainData,
|
|
3342
|
+
toolPositions: data.toolPositions,
|
|
3343
|
+
step: data.step,
|
|
3344
|
+
gridStep: data.gridStep,
|
|
3345
|
+
terrainBounds: data.terrainBounds,
|
|
3346
|
+
zFloor: data.zFloor,
|
|
3347
|
+
onProgress: (progressData) => {
|
|
3348
|
+
self.postMessage({
|
|
3349
|
+
type: "tracing-progress",
|
|
3350
|
+
data: progressData.data
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
3354
|
+
const tracingTransferBuffers = tracingResult.paths.map((p) => p.buffer);
|
|
3355
|
+
self.postMessage({
|
|
3356
|
+
type: "tracing-toolpaths-complete",
|
|
3357
|
+
data: tracingResult
|
|
3358
|
+
}, tracingTransferBuffers);
|
|
3359
|
+
break;
|
|
3360
|
+
case "create-tracing-buffers":
|
|
3361
|
+
createReusableTracingBuffers(data.terrainPositions, data.toolPositions);
|
|
3362
|
+
self.postMessage({
|
|
3363
|
+
type: "tracing-buffers-created",
|
|
3364
|
+
data: { success: true }
|
|
3365
|
+
});
|
|
3366
|
+
break;
|
|
3367
|
+
case "destroy-tracing-buffers":
|
|
3368
|
+
destroyReusableTracingBuffers();
|
|
3369
|
+
self.postMessage({
|
|
3370
|
+
type: "tracing-buffers-destroyed",
|
|
3371
|
+
data: { success: true }
|
|
3372
|
+
});
|
|
3373
|
+
break;
|
|
2429
3374
|
case "calibrate":
|
|
2430
3375
|
const calibrationResult = await calibrateGPU(device, data?.options || {});
|
|
2431
3376
|
self.postMessage({
|