@gridspace/raster-path 1.0.7 → 1.0.9

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.
@@ -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(`YGWIPII2 | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}\xB0) INPUT terrain first 5 Z values: ${strip.positions.slice(0, 5).map((v) => v.toFixed(3)).join(",")}`);
2390
+ debug.log(`QRSDOFON | 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(`YGWIPII2 | Strip ${globalStripIdx} (${strip.angle.toFixed(1)}\xB0) OUTPUT toolpath first 5 Z values: ${stripToolpathResult.pathData.slice(0, 5).map((v) => v.toFixed(3)).join(",")}`);
2403
+ debug.log(`QRSDOFON | 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) {
@@ -2339,59 +2862,109 @@ async function generateTracingToolpaths({
2339
2862
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
2340
2863
  });
2341
2864
  device.queue.writeBuffer(maxZBuffer, 0, maxZInitData);
2342
- const outputPaths = [];
2865
+ debug.log("PHASE 1: Sampling all paths...");
2866
+ const pathIndex = [];
2867
+ const sampledSegments = [];
2343
2868
  let totalSampledPoints = 0;
2344
2869
  for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
2345
- const pathStartTime = performance.now();
2346
2870
  const inputPath = paths[pathIdx];
2347
- debug.log(`Processing path ${pathIdx + 1}/${paths.length}: ${inputPath.length / 2} input vertices`);
2871
+ debug.log(`Path ${pathIdx + 1}/${paths.length}: ${inputPath.length / 2} input vertices`);
2348
2872
  const sampledPath = samplePath(inputPath, step);
2349
- const numSampledPoints = sampledPath.length / 2;
2350
- totalSampledPoints += numSampledPoints;
2351
- debug.log(` Sampled to ${numSampledPoints} points`);
2352
- const firstX = sampledPath[0];
2353
- const firstY = sampledPath[1];
2873
+ const numPoints = sampledPath.length / 2;
2874
+ pathIndex.push({
2875
+ startOffset: totalSampledPoints,
2876
+ endOffset: totalSampledPoints + numPoints,
2877
+ numPoints
2878
+ });
2879
+ sampledSegments.push(sampledPath);
2880
+ totalSampledPoints += numPoints;
2881
+ debug.log(` Sampled to ${numPoints} points`);
2882
+ }
2883
+ const unifiedSampledXY = new Float32Array(totalSampledPoints * 2);
2884
+ let writeOffset = 0;
2885
+ for (let pathIdx = 0; pathIdx < sampledSegments.length; pathIdx++) {
2886
+ const sampledPath = sampledSegments[pathIdx];
2887
+ unifiedSampledXY.set(sampledPath, writeOffset * 2);
2888
+ writeOffset += sampledPath.length / 2;
2889
+ }
2890
+ debug.log(`Unified buffer: ${totalSampledPoints} total points from ${paths.length} paths`);
2891
+ if (totalSampledPoints > 0) {
2892
+ const firstX = unifiedSampledXY[0];
2893
+ const firstY = unifiedSampledXY[1];
2354
2894
  const gridX = (firstX - terrainBounds.min.x) / gridStep;
2355
2895
  const gridY = (firstY - terrainBounds.min.y) / gridStep;
2356
- debug.log(` First point: world(${firstX.toFixed(2)}, ${firstY.toFixed(2)}) -> grid(${gridX.toFixed(2)}, ${gridY.toFixed(2)})`);
2357
- 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)})`);
2358
- const inputBufferSize = sampledPath.byteLength;
2359
- const outputBufferSize = numSampledPoints * 4;
2360
- const estimatedMemory = inputBufferSize + outputBufferSize;
2361
- const configuredLimit = config.maxGPUMemoryMB * 1024 * 1024;
2362
- const deviceLimit = deviceCapabilities.maxStorageBufferBindingSize;
2363
- const maxSafeSize = Math.min(configuredLimit, deviceLimit) * config.gpuMemorySafetyMargin;
2364
- if (estimatedMemory > maxSafeSize) {
2896
+ debug.log(`First point: world(${firstX.toFixed(2)}, ${firstY.toFixed(2)}) -> grid(${gridX.toFixed(2)}, ${gridY.toFixed(2)})`);
2897
+ }
2898
+ debug.log("PHASE 2: Calculating memory budget and chunking...");
2899
+ const bytesPerPoint = 8 + 4;
2900
+ const configuredLimit = config.maxGPUMemoryMB * 1024 * 1024;
2901
+ const deviceLimit = deviceCapabilities.maxStorageBufferBindingSize;
2902
+ const maxSafeSize = Math.min(configuredLimit, deviceLimit) * config.gpuMemorySafetyMargin;
2903
+ const fixedOverhead = terrainPositions.byteLength + sparseToolData.count * 16 + paths.length * 4 + 48;
2904
+ if (fixedOverhead > maxSafeSize) {
2905
+ if (shouldCleanupBuffers) {
2365
2906
  terrainBuffer.destroy();
2366
2907
  toolBuffer.destroy();
2367
- throw new Error(
2368
- `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.`
2369
- );
2370
2908
  }
2371
- const inputBuffer = device.createBuffer({
2372
- size: sampledPath.byteLength,
2373
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
2374
- });
2375
- device.queue.writeBuffer(inputBuffer, 0, sampledPath);
2376
- const outputBuffer = device.createBuffer({
2377
- size: outputBufferSize,
2378
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
2909
+ throw new Error(
2910
+ `Fixed buffers (terrain + tool) exceed GPU memory: ${(fixedOverhead / 1024 / 1024).toFixed(1)}MB > ${(maxSafeSize / 1024 / 1024).toFixed(1)}MB. Try reducing terrain resolution or tool density.`
2911
+ );
2912
+ }
2913
+ const availableForPaths = maxSafeSize - fixedOverhead;
2914
+ const maxPointsPerChunk = Math.floor(availableForPaths / bytesPerPoint);
2915
+ debug.log(`Memory budget: ${(maxSafeSize / 1024 / 1024).toFixed(1)}MB safe, ${(availableForPaths / 1024 / 1024).toFixed(1)}MB available for paths`);
2916
+ debug.log(`Max points per chunk: ${maxPointsPerChunk.toLocaleString()}`);
2917
+ const chunks = [];
2918
+ let currentStart = 0;
2919
+ while (currentStart < totalSampledPoints) {
2920
+ const currentEnd = Math.min(currentStart + maxPointsPerChunk, totalSampledPoints);
2921
+ chunks.push({
2922
+ startPoint: currentStart,
2923
+ endPoint: currentEnd,
2924
+ numPoints: currentEnd - currentStart
2379
2925
  });
2926
+ currentStart = currentEnd;
2927
+ }
2928
+ debug.log(`Created ${chunks.length} chunk(s) for processing`);
2929
+ debug.log("PHASE 3: Creating reusable GPU buffers...");
2930
+ const inputBuffer = device.createBuffer({
2931
+ size: maxPointsPerChunk * 8,
2932
+ // 2 floats per point
2933
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
2934
+ });
2935
+ const outputBuffer = device.createBuffer({
2936
+ size: maxPointsPerChunk * 4,
2937
+ // 1 float per point
2938
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
2939
+ });
2940
+ const uniformBuffer = device.createBuffer({
2941
+ size: 48,
2942
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2943
+ });
2944
+ const stagingBuffer = device.createBuffer({
2945
+ size: maxPointsPerChunk * 4,
2946
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
2947
+ });
2948
+ const unifiedOutputZ = new Float32Array(totalSampledPoints);
2949
+ debug.log(`Buffers created for ${maxPointsPerChunk.toLocaleString()} points per chunk`);
2950
+ debug.log("PHASE 4: Processing chunks...");
2951
+ for (let chunkIdx = 0; chunkIdx < chunks.length; chunkIdx++) {
2952
+ const chunk = chunks[chunkIdx];
2953
+ const { startPoint, endPoint, numPoints } = chunk;
2954
+ debug.log(`Processing chunk ${chunkIdx + 1}/${chunks.length}: points ${startPoint}-${endPoint} (${numPoints} points)`);
2955
+ const chunkInputXY = unifiedSampledXY.subarray(startPoint * 2, endPoint * 2);
2956
+ device.queue.writeBuffer(inputBuffer, 0, chunkInputXY);
2380
2957
  const uniformData = new Uint32Array(12);
2381
2958
  uniformData[0] = terrainData.width;
2382
2959
  uniformData[1] = terrainData.height;
2383
2960
  uniformData[2] = sparseToolData.count;
2384
- uniformData[3] = numSampledPoints;
2385
- uniformData[4] = pathIdx;
2961
+ uniformData[3] = numPoints;
2962
+ uniformData[4] = 0;
2386
2963
  const uniformDataFloat = new Float32Array(uniformData.buffer);
2387
2964
  uniformDataFloat[5] = terrainBounds.min.x;
2388
2965
  uniformDataFloat[6] = terrainBounds.min.y;
2389
2966
  uniformDataFloat[7] = gridStep;
2390
2967
  uniformDataFloat[8] = zFloor;
2391
- const uniformBuffer = device.createBuffer({
2392
- size: uniformData.byteLength,
2393
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
2394
- });
2395
2968
  device.queue.writeBuffer(uniformBuffer, 0, uniformData);
2396
2969
  await device.queue.onSubmittedWorkDone();
2397
2970
  const bindGroup = device.createBindGroup({
@@ -2402,6 +2975,7 @@ async function generateTracingToolpaths({
2402
2975
  { binding: 2, resource: { buffer: inputBuffer } },
2403
2976
  { binding: 3, resource: { buffer: outputBuffer } },
2404
2977
  { binding: 4, resource: { buffer: maxZBuffer } },
2978
+ // Keep for shader compatibility
2405
2979
  { binding: 5, resource: { buffer: uniformBuffer } }
2406
2980
  ]
2407
2981
  });
@@ -2409,60 +2983,60 @@ async function generateTracingToolpaths({
2409
2983
  const passEncoder = commandEncoder.beginComputePass();
2410
2984
  passEncoder.setPipeline(cachedTracingPipeline);
2411
2985
  passEncoder.setBindGroup(0, bindGroup);
2412
- const workgroupsX = Math.ceil(numSampledPoints / 64);
2986
+ const workgroupsX = Math.ceil(numPoints / 64);
2413
2987
  passEncoder.dispatchWorkgroups(workgroupsX);
2414
2988
  passEncoder.end();
2415
- const stagingBuffer = device.createBuffer({
2416
- size: outputBufferSize,
2417
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
2418
- });
2419
- commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, outputBufferSize);
2989
+ commandEncoder.copyBufferToBuffer(outputBuffer, 0, stagingBuffer, 0, numPoints * 4);
2420
2990
  device.queue.submit([commandEncoder.finish()]);
2421
2991
  await device.queue.onSubmittedWorkDone();
2422
2992
  await stagingBuffer.mapAsync(GPUMapMode.READ);
2423
- const outputDepths = new Float32Array(stagingBuffer.getMappedRange());
2424
- const depthsCopy = new Float32Array(outputDepths);
2993
+ const chunkOutputZ = new Float32Array(stagingBuffer.getMappedRange(), 0, numPoints);
2994
+ unifiedOutputZ.set(chunkOutputZ, startPoint);
2425
2995
  stagingBuffer.unmap();
2426
- const outputXYZ = new Float32Array(numSampledPoints * 3);
2427
- for (let i = 0; i < numSampledPoints; i++) {
2428
- outputXYZ[i * 3 + 0] = sampledPath[i * 2 + 0];
2429
- outputXYZ[i * 3 + 1] = sampledPath[i * 2 + 1];
2430
- outputXYZ[i * 3 + 2] = depthsCopy[i];
2431
- }
2432
- outputPaths.push(outputXYZ);
2433
- inputBuffer.destroy();
2434
- outputBuffer.destroy();
2435
- uniformBuffer.destroy();
2436
- stagingBuffer.destroy();
2437
- const pathTime = performance.now() - pathStartTime;
2438
- debug.log(` Path ${pathIdx + 1} complete: ${numSampledPoints} points in ${pathTime.toFixed(1)}ms`);
2996
+ debug.log(` Chunk ${chunkIdx + 1} complete: ${numPoints} points processed`);
2439
2997
  if (onProgress) {
2440
2998
  onProgress({
2441
2999
  type: "tracing-progress",
2442
3000
  data: {
2443
- percent: Math.round((pathIdx + 1) / paths.length * 100),
2444
- current: pathIdx + 1,
2445
- total: paths.length,
2446
- pathIndex: pathIdx
3001
+ percent: Math.round(endPoint / totalSampledPoints * 100),
3002
+ current: endPoint,
3003
+ total: totalSampledPoints,
3004
+ chunkIndex: chunkIdx + 1,
3005
+ totalChunks: chunks.length
2447
3006
  }
2448
3007
  });
2449
3008
  }
2450
3009
  }
2451
- const maxZStagingBuffer = device.createBuffer({
2452
- size: maxZInitData.byteLength,
2453
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
2454
- });
2455
- const maxZCommandEncoder = device.createCommandEncoder();
2456
- maxZCommandEncoder.copyBufferToBuffer(maxZBuffer, 0, maxZStagingBuffer, 0, maxZInitData.byteLength);
2457
- device.queue.submit([maxZCommandEncoder.finish()]);
2458
- await device.queue.onSubmittedWorkDone();
2459
- await maxZStagingBuffer.mapAsync(GPUMapMode.READ);
2460
- const maxZBitsI32 = new Int32Array(maxZStagingBuffer.getMappedRange());
2461
- const maxZBitsCopy = new Int32Array(maxZBitsI32);
2462
- maxZStagingBuffer.unmap();
2463
- const maxZValues = new Float32Array(maxZBitsCopy.buffer);
3010
+ inputBuffer.destroy();
3011
+ outputBuffer.destroy();
3012
+ uniformBuffer.destroy();
3013
+ stagingBuffer.destroy();
3014
+ debug.log("All chunks processed");
3015
+ debug.log("PHASE 5: Remapping to individual paths and computing maxZ...");
3016
+ const outputPaths = [];
3017
+ const maxZValues = new Array(paths.length).fill(zFloor);
3018
+ for (let pathIdx = 0; pathIdx < pathIndex.length; pathIdx++) {
3019
+ const { startOffset, numPoints } = pathIndex[pathIdx];
3020
+ if (numPoints === 0) {
3021
+ outputPaths.push(new Float32Array(0));
3022
+ debug.log(`Path ${pathIdx + 1}: empty`);
3023
+ continue;
3024
+ }
3025
+ const pathXYZ = new Float32Array(numPoints * 3);
3026
+ for (let i = 0; i < numPoints; i++) {
3027
+ const unifiedIdx = startOffset + i;
3028
+ const x = unifiedSampledXY[unifiedIdx * 2 + 0];
3029
+ const y = unifiedSampledXY[unifiedIdx * 2 + 1];
3030
+ const z = unifiedOutputZ[unifiedIdx];
3031
+ pathXYZ[i * 3 + 0] = x;
3032
+ pathXYZ[i * 3 + 1] = y;
3033
+ pathXYZ[i * 3 + 2] = z;
3034
+ maxZValues[pathIdx] = Math.max(maxZValues[pathIdx], z);
3035
+ }
3036
+ outputPaths.push(pathXYZ);
3037
+ debug.log(`Path ${pathIdx + 1}: ${numPoints} points, maxZ=${maxZValues[pathIdx].toFixed(2)}`);
3038
+ }
2464
3039
  maxZBuffer.destroy();
2465
- maxZStagingBuffer.destroy();
2466
3040
  if (shouldCleanupBuffers) {
2467
3041
  terrainBuffer.destroy();
2468
3042
  toolBuffer.destroy();
@@ -2803,6 +3377,14 @@ self.onmessage = async function(e) {
2803
3377
  data: radialToolpathResult
2804
3378
  }, toolpathTransferBuffers);
2805
3379
  break;
3380
+ case "radial-generate-toolpaths-v3":
3381
+ const radialV3ToolpathResult = await generateRadialToolpathsV3(data);
3382
+ const v3ToolpathTransferBuffers = radialV3ToolpathResult.strips.map((strip) => strip.pathData.buffer);
3383
+ self.postMessage({
3384
+ type: "radial-toolpaths-complete",
3385
+ data: radialV3ToolpathResult
3386
+ }, v3ToolpathTransferBuffers);
3387
+ break;
2806
3388
  case "tracing-generate-toolpaths":
2807
3389
  const tracingResult = await generateTracingToolpaths({
2808
3390
  paths: data.paths,