@hello-terrain/three 0.0.0-alpha.12 → 0.0.0-alpha.13

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/dist/index.cjs CHANGED
@@ -937,20 +937,15 @@ function beginUpdate(state, topology, params) {
937
937
 
938
938
  function shouldSplit(bounds, level, maxLevel, params) {
939
939
  if (level >= maxLevel) return false;
940
- const mode = params.mode ?? "distance";
941
940
  const cx = bounds.cx;
942
941
  const cy = bounds.cy;
943
942
  const cz = bounds.cz;
944
943
  const distSq = cx * cx + cy * cy + cz * cz;
945
944
  const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
946
- if (mode === "screen") {
947
- const proj = params.projectionFactor ?? 0;
948
- const target = params.targetPixels ?? 0;
949
- if (proj <= 0 || target <= 0) {
950
- const f2 = params.distanceFactor ?? 2;
951
- const threshold2 = bounds.r * f2;
952
- return safeDistSq < threshold2 * threshold2;
953
- }
945
+ if (params.mode === "screen") {
946
+ const proj = params.projectionFactor;
947
+ const target = params.targetPixels;
948
+ if (proj <= 0 || target <= 0) return false;
954
949
  const left = bounds.r * bounds.r * proj * proj;
955
950
  const right = safeDistSq * target * target;
956
951
  return left > right;
@@ -984,7 +979,7 @@ function refineLeaves(state, topology, params, outLeaves) {
984
979
  let elevationRange;
985
980
  if (params.tileElevationRange) {
986
981
  const range = state.scratchElevationRange;
987
- if (params.tileElevationRange(space, level, x, y, range)) {
982
+ if (params.tileElevationRange(tile, range)) {
988
983
  elevationRange = range;
989
984
  }
990
985
  }
@@ -1087,19 +1082,9 @@ function balance2to1(state, topology, params, leaves) {
1087
1082
  }
1088
1083
 
1089
1084
  function update(state, topology, params, outLeaves) {
1090
- const cam = params.cameraOrigin;
1091
- const elevation = params.elevationAtCameraXZ ?? 0;
1092
- const origX = cam.x;
1093
- const origY = cam.y;
1094
- const origZ = cam.z;
1095
- topology.projection.cpu.cameraSurfaceOffset(cam, elevation);
1096
1085
  beginUpdate(state, topology, params);
1097
1086
  const leaves = refineLeaves(state, topology, params, outLeaves);
1098
- const result = balance2to1(state, topology, params, leaves);
1099
- cam.x = origX;
1100
- cam.y = origY;
1101
- cam.z = origZ;
1102
- return result;
1087
+ return balance2to1(state, topology, params, leaves);
1103
1088
  }
1104
1089
 
1105
1090
  const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
@@ -1500,26 +1485,6 @@ function cpuRaycast(query, ray, config, options) {
1500
1485
  distance: ray.origin.distanceTo(point)
1501
1486
  };
1502
1487
  }
1503
- function cpuRaycastBoundsOnly(ray, config, options) {
1504
- const bounds = getTerrainBounds(config);
1505
- const planeY = (config.minY + config.maxY) * 0.5;
1506
- const dirY = ray.direction.y;
1507
- if (Math.abs(dirY) < 1e-8) return null;
1508
- const t = (planeY - ray.origin.y) / dirY;
1509
- if (t < 0) return null;
1510
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1511
- if (t > maxDistance) return null;
1512
- const point = new three.Vector3();
1513
- ray.at(t, point);
1514
- if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
1515
- return null;
1516
- }
1517
- return {
1518
- position: point,
1519
- normal: new three.Vector3(0, 1, 0),
1520
- distance: ray.origin.distanceTo(point)
1521
- };
1522
- }
1523
1488
  function intersectRaySphere(ray, cx, cy, cz, radius) {
1524
1489
  const ox = ray.origin.x - cx;
1525
1490
  const oy = ray.origin.y - cy;
@@ -1585,22 +1550,6 @@ function cubeSphereRaycast(query, ray, params, options) {
1585
1550
  distance: ray.origin.distanceTo(sample.position)
1586
1551
  };
1587
1552
  }
1588
- function cubeSphereRaycastBoundsOnly(ray, params, options) {
1589
- const shell = intersectRaySphere(ray, params.centerX, params.centerY, params.centerZ, params.radius);
1590
- if (!shell) return null;
1591
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1592
- const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1593
- if (t < 0 || t > maxDistance) return null;
1594
- const point = new three.Vector3();
1595
- ray.at(t, point);
1596
- const normal = new three.Vector3(
1597
- point.x - params.centerX,
1598
- point.y - params.centerY,
1599
- point.z - params.centerZ
1600
- ).normalize();
1601
- if (params.invert) normal.negate();
1602
- return { position: point, normal, distance: ray.origin.distanceTo(point) };
1603
- }
1604
1553
  function torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams) {
1605
1554
  positionToTorusParams(
1606
1555
  px,
@@ -1655,28 +1604,6 @@ function torusRaycast(query, ray, params, options) {
1655
1604
  distance: ray.origin.distanceTo(sample.position)
1656
1605
  };
1657
1606
  }
1658
- function torusRaycastBoundsOnly(ray, params, options) {
1659
- const shell = intersectRaySphere(
1660
- ray,
1661
- params.centerX,
1662
- params.centerY,
1663
- params.centerZ,
1664
- params.outerRadius
1665
- );
1666
- if (!shell) return null;
1667
- const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
1668
- const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
1669
- if (t < 0 || t > maxDistance) return null;
1670
- const point = new three.Vector3();
1671
- ray.at(t, point);
1672
- const normal = new three.Vector3(
1673
- point.x - params.centerX,
1674
- point.y - params.centerY,
1675
- point.z - params.centerZ
1676
- ).normalize();
1677
- if (params.invert) normal.negate();
1678
- return { position: point, normal, distance: ray.origin.distanceTo(point) };
1679
- }
1680
1607
 
1681
1608
  function createTerrainQuery(cache) {
1682
1609
  return {
@@ -1802,9 +1729,6 @@ function createFlatProjection() {
1802
1729
  }
1803
1730
  },
1804
1731
  cpu: {
1805
- cameraSurfaceOffset(cam, elevation) {
1806
- cam.y -= elevation;
1807
- },
1808
1732
  createSurfaceOps() {
1809
1733
  return null;
1810
1734
  },
@@ -1813,26 +1737,44 @@ function createFlatProjection() {
1813
1737
  },
1814
1738
  raycast(ctx) {
1815
1739
  const { ray, options, terrainQuery, config } = ctx;
1816
- if (terrainQuery) {
1817
- const precise = cpuRaycast(terrainQuery, ray, config, options);
1818
- if (precise) return precise;
1819
- }
1820
- const coarse = cpuRaycastBoundsOnly(ray, config, options);
1821
- if (coarse && terrainQuery) {
1822
- const sample = terrainQuery.sampleTerrain(coarse.position.x, coarse.position.z);
1823
- if (sample.valid) {
1824
- coarse.position.y = sample.elevation;
1825
- coarse.normal.copy(sample.normal);
1826
- }
1827
- }
1828
- return coarse;
1740
+ if (!terrainQuery) return null;
1741
+ return cpuRaycast(terrainQuery, ray, config, options);
1829
1742
  }
1830
1743
  }
1831
1744
  };
1832
1745
  }
1833
1746
 
1747
+ function boundingSphereFromPoints(px, py, pz, count, cameraOrigin, out) {
1748
+ let sumX = 0;
1749
+ let sumY = 0;
1750
+ let sumZ = 0;
1751
+ for (let i = 0; i < count; i++) {
1752
+ sumX += px[i];
1753
+ sumY += py[i];
1754
+ sumZ += pz[i];
1755
+ }
1756
+ const cX = sumX / count;
1757
+ const cY = sumY / count;
1758
+ const cZ = sumZ / count;
1759
+ let maxDistSq = 0;
1760
+ for (let i = 0; i < count; i++) {
1761
+ const dx = px[i] - cX;
1762
+ const dy = py[i] - cY;
1763
+ const dz = pz[i] - cZ;
1764
+ const dSq = dx * dx + dy * dy + dz * dz;
1765
+ if (dSq > maxDistSq) maxDistSq = dSq;
1766
+ }
1767
+ out.cx = cX - cameraOrigin.x;
1768
+ out.cy = cY - cameraOrigin.y;
1769
+ out.cz = cZ - cameraOrigin.z;
1770
+ out.r = Math.sqrt(maxDistSq);
1771
+ }
1772
+
1834
1773
  function createFlatTopology(cfg) {
1835
1774
  const halfRoot = 0.5 * cfg.rootSize;
1775
+ const px = new Float64Array(8);
1776
+ const py = new Float64Array(8);
1777
+ const pz = new Float64Array(8);
1836
1778
  const topology = {
1837
1779
  spaceCount: 1,
1838
1780
  maxRootCount: 1,
@@ -1872,15 +1814,26 @@ function createFlatTopology(cfg) {
1872
1814
  const size = cfg.rootSize * scale;
1873
1815
  const minX = cfg.origin.x + (tile.x * size - halfRoot);
1874
1816
  const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1875
- const centerX = minX + 0.5 * size;
1876
- const centerZ = minZ + 0.5 * size;
1877
- const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1878
- out.cx = centerX - cameraOrigin.x;
1879
- out.cy = centerY - cameraOrigin.y;
1880
- out.cz = centerZ - cameraOrigin.z;
1881
- const halfDiag = 0.7071067811865476 * size;
1882
- const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1883
- out.r = halfDiag + vertExtent;
1817
+ const maxX = minX + size;
1818
+ const maxZ = minZ + size;
1819
+ const yLo = cfg.origin.y + (elevationRange ? elevationRange.min : 0);
1820
+ const yHi = elevationRange ? cfg.origin.y + elevationRange.max : 0;
1821
+ let pointCount = 0;
1822
+ for (let i = 0; i < 4; i++) {
1823
+ const cornerX = (i & 1) === 0 ? minX : maxX;
1824
+ const cornerZ = i < 2 ? minZ : maxZ;
1825
+ px[pointCount] = cornerX;
1826
+ py[pointCount] = yLo;
1827
+ pz[pointCount] = cornerZ;
1828
+ pointCount += 1;
1829
+ if (elevationRange) {
1830
+ px[pointCount] = cornerX;
1831
+ py[pointCount] = yHi;
1832
+ pz[pointCount] = cornerZ;
1833
+ pointCount += 1;
1834
+ }
1835
+ }
1836
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
1884
1837
  },
1885
1838
  rootTiles(_cameraOrigin, out) {
1886
1839
  const root = out[0];
@@ -1898,6 +1851,9 @@ function createInfiniteFlatTopology(cfg) {
1898
1851
  const halfRoot = 0.5 * cfg.rootSize;
1899
1852
  const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
1900
1853
  const rootWidth = rootGridRadius * 2 + 1;
1854
+ const px = new Float64Array(8);
1855
+ const py = new Float64Array(8);
1856
+ const pz = new Float64Array(8);
1901
1857
  return {
1902
1858
  spaceCount: 1,
1903
1859
  maxRootCount: rootWidth * rootWidth,
@@ -1931,15 +1887,26 @@ function createInfiniteFlatTopology(cfg) {
1931
1887
  const size = cfg.rootSize * scale;
1932
1888
  const minX = cfg.origin.x + (tile.x * size - halfRoot);
1933
1889
  const minZ = cfg.origin.z + (tile.y * size - halfRoot);
1934
- const centerX = minX + 0.5 * size;
1935
- const centerZ = minZ + 0.5 * size;
1936
- const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
1937
- out.cx = centerX - cameraOrigin.x;
1938
- out.cy = centerY - cameraOrigin.y;
1939
- out.cz = centerZ - cameraOrigin.z;
1940
- const halfDiag = 0.7071067811865476 * size;
1941
- const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
1942
- out.r = halfDiag + vertExtent;
1890
+ const maxX = minX + size;
1891
+ const maxZ = minZ + size;
1892
+ const yLo = cfg.origin.y + (elevationRange ? elevationRange.min : 0);
1893
+ const yHi = elevationRange ? cfg.origin.y + elevationRange.max : 0;
1894
+ let pointCount = 0;
1895
+ for (let i = 0; i < 4; i++) {
1896
+ const cornerX = (i & 1) === 0 ? minX : maxX;
1897
+ const cornerZ = i < 2 ? minZ : maxZ;
1898
+ px[pointCount] = cornerX;
1899
+ py[pointCount] = yLo;
1900
+ pz[pointCount] = cornerZ;
1901
+ pointCount += 1;
1902
+ if (elevationRange) {
1903
+ px[pointCount] = cornerX;
1904
+ py[pointCount] = yHi;
1905
+ pz[pointCount] = cornerZ;
1906
+ pointCount += 1;
1907
+ }
1908
+ }
1909
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
1943
1910
  },
1944
1911
  rootTiles(cameraOrigin, out) {
1945
1912
  const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
@@ -2475,7 +2442,6 @@ function createCubeSphereProjection(config) {
2475
2442
  const nx = dx / len;
2476
2443
  const ny = dy / len;
2477
2444
  const nz = dz / len;
2478
- const dirSign = invert ? -1 : 1;
2479
2445
  dirScratch[0] = nx;
2480
2446
  dirScratch[1] = ny;
2481
2447
  dirScratch[2] = nz;
@@ -2484,9 +2450,9 @@ function createCubeSphereProjection(config) {
2484
2450
  out.space = face;
2485
2451
  out.u = uvScratch[0];
2486
2452
  out.v = uvScratch[1];
2487
- out.dirX = nx * dirSign;
2488
- out.dirY = ny * dirSign;
2489
- out.dirZ = nz * dirSign;
2453
+ out.dirX = nx;
2454
+ out.dirY = ny;
2455
+ out.dirZ = nz;
2490
2456
  return true;
2491
2457
  },
2492
2458
  surfacePosition(key, elevation, outVec) {
@@ -2519,7 +2485,8 @@ function createCubeSphereProjection(config) {
2519
2485
  let nx = tuy * tvz - tuz * tvy;
2520
2486
  let ny = tuz * tvx - tux * tvz;
2521
2487
  let nz = tux * tvy - tuy * tvx;
2522
- if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
2488
+ const outwardSign = invert ? -1 : 1;
2489
+ if ((nx * key.dirX + ny * key.dirY + nz * key.dirZ) * outwardSign < 0) {
2523
2490
  nx = -nx;
2524
2491
  ny = -ny;
2525
2492
  nz = -nz;
@@ -2573,19 +2540,6 @@ function createCubeSphereProjection(config) {
2573
2540
  augmentSampler: augmentCubeSphereSampler
2574
2541
  },
2575
2542
  cpu: {
2576
- cameraSurfaceOffset(cam, elevation) {
2577
- const dx = cam.x - center.x;
2578
- const dy = cam.y - center.y;
2579
- const dz = cam.z - center.z;
2580
- const len = Math.hypot(dx, dy, dz);
2581
- if (len > 1e-12) {
2582
- const sign = invert ? 1 : -1;
2583
- const inv = sign * elevation / len;
2584
- cam.x += dx * inv;
2585
- cam.y += dy * inv;
2586
- cam.z += dz * inv;
2587
- }
2588
- },
2589
2543
  createSurfaceOps() {
2590
2544
  return surfaceOps;
2591
2545
  },
@@ -2596,6 +2550,7 @@ function createCubeSphereProjection(config) {
2596
2550
  return { query, surfaceQuery, sphereQuery };
2597
2551
  },
2598
2552
  raycast(ctx) {
2553
+ if (!ctx.sphereQuery) return null;
2599
2554
  const range = ctx.terrainQuery?.getGlobalElevationRange();
2600
2555
  const dispMax = range ? Math.max(0, range.max - center.y) : radius * 0.1;
2601
2556
  const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING$1;
@@ -2607,11 +2562,7 @@ function createCubeSphereProjection(config) {
2607
2562
  maxRadius: radius + outerPadding,
2608
2563
  invert
2609
2564
  };
2610
- if (ctx.sphereQuery) {
2611
- const precise = cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2612
- if (precise) return precise;
2613
- }
2614
- return cubeSphereRaycastBoundsOnly(ctx.ray, params, ctx.options);
2565
+ return cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
2615
2566
  }
2616
2567
  }
2617
2568
  };
@@ -2702,8 +2653,6 @@ function createCubeSphereTopology(cfg) {
2702
2653
  spaceCount: 6,
2703
2654
  maxRootCount: 6,
2704
2655
  projection: createCubeSphereProjection({ radius, center, invert: cfg.invert }),
2705
- radius,
2706
- center,
2707
2656
  neighborSameLevel(tile, dir, out) {
2708
2657
  const level = tile.level;
2709
2658
  const n = 1 << level;
@@ -2740,48 +2689,29 @@ function createCubeSphereTopology(cfg) {
2740
2689
  const u1 = (tile.x + 1) / n;
2741
2690
  const v0 = tile.y / n;
2742
2691
  const v1 = (tile.y + 1) / n;
2743
- const cornersU = [u0, u1, u0, u1];
2744
- const cornersV = [v0, v0, v1, v1];
2745
- const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2692
+ const shellLo = radius + (elevationRange ? elevationRange.min : 0);
2693
+ const shellHi = elevationRange ? radius + elevationRange.max : 0;
2746
2694
  let pointCount = 0;
2747
- let sumX = 0;
2748
- let sumY = 0;
2749
- let sumZ = 0;
2750
2695
  for (let i = 0; i < 4; i++) {
2751
- faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
2696
+ const u = (i & 1) === 0 ? u0 : u1;
2697
+ const v = i < 2 ? v0 : v1;
2698
+ faceUVToCube(tile.space, u, v, cube);
2752
2699
  const len = Math.hypot(cube[0], cube[1], cube[2]);
2753
2700
  const dirX = cube[0] / len;
2754
2701
  const dirY = cube[1] / len;
2755
2702
  const dirZ = cube[2] / len;
2756
- for (let di = 0; di < disps.length; di++) {
2757
- const shellRadius = radius + disps[di];
2758
- const sx = center.x + dirX * shellRadius;
2759
- const sy = center.y + dirY * shellRadius;
2760
- const sz = center.z + dirZ * shellRadius;
2761
- px[pointCount] = sx;
2762
- py[pointCount] = sy;
2763
- pz[pointCount] = sz;
2764
- sumX += sx;
2765
- sumY += sy;
2766
- sumZ += sz;
2703
+ px[pointCount] = center.x + dirX * shellLo;
2704
+ py[pointCount] = center.y + dirY * shellLo;
2705
+ pz[pointCount] = center.z + dirZ * shellLo;
2706
+ pointCount += 1;
2707
+ if (elevationRange) {
2708
+ px[pointCount] = center.x + dirX * shellHi;
2709
+ py[pointCount] = center.y + dirY * shellHi;
2710
+ pz[pointCount] = center.z + dirZ * shellHi;
2767
2711
  pointCount += 1;
2768
2712
  }
2769
2713
  }
2770
- const cX = sumX / pointCount;
2771
- const cY = sumY / pointCount;
2772
- const cZ = sumZ / pointCount;
2773
- let maxDistSq = 0;
2774
- for (let i = 0; i < pointCount; i++) {
2775
- const dx = px[i] - cX;
2776
- const dy = py[i] - cY;
2777
- const dz = pz[i] - cZ;
2778
- const dSq = dx * dx + dy * dy + dz * dz;
2779
- if (dSq > maxDistSq) maxDistSq = dSq;
2780
- }
2781
- out.cx = cX - cameraOrigin.x;
2782
- out.cy = cY - cameraOrigin.y;
2783
- out.cz = cZ - cameraOrigin.z;
2784
- out.r = Math.sqrt(maxDistSq);
2714
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
2785
2715
  },
2786
2716
  rootTiles(_cameraOrigin, out) {
2787
2717
  for (let s = 0; s < 6; s++) {
@@ -2952,13 +2882,6 @@ function createTorusProjection(config) {
2952
2882
  }
2953
2883
  },
2954
2884
  cpu: {
2955
- cameraSurfaceOffset(cam, elevation) {
2956
- positionToTorusParams(cam.x, cam.y, cam.z, majorRadius, center, params);
2957
- torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
2958
- cam.x -= normalScratch[0] * elevation;
2959
- cam.y -= normalScratch[1] * elevation;
2960
- cam.z -= normalScratch[2] * elevation;
2961
- },
2962
2885
  createSurfaceOps() {
2963
2886
  return surfaceOps;
2964
2887
  },
@@ -2968,6 +2891,7 @@ function createTorusProjection(config) {
2968
2891
  return { query, surfaceQuery, sphereQuery: null };
2969
2892
  },
2970
2893
  raycast(ctx) {
2894
+ if (!ctx.surfaceQuery) return null;
2971
2895
  const range = ctx.terrainQuery?.getGlobalElevationRange();
2972
2896
  const dispMax = range ? Math.max(0, range.max - ctx.config.originY) : minorRadius * 0.5;
2973
2897
  const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING;
@@ -2980,11 +2904,7 @@ function createTorusProjection(config) {
2980
2904
  outerRadius: majorRadius + minorRadius + outerPadding,
2981
2905
  invert
2982
2906
  };
2983
- if (ctx.surfaceQuery) {
2984
- const precise = torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2985
- if (precise) return precise;
2986
- }
2987
- return torusRaycastBoundsOnly(ctx.ray, raycastParams, ctx.options);
2907
+ return torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
2988
2908
  }
2989
2909
  }
2990
2910
  };
@@ -3017,8 +2937,6 @@ function createTorusTopology(cfg) {
3017
2937
  baseU,
3018
2938
  baseV
3019
2939
  }),
3020
- radius: majorRadius + minorRadius,
3021
- center,
3022
2940
  neighborSameLevel(tile, dir, out) {
3023
2941
  const { nU, nV } = levelResolution(tile.level);
3024
2942
  let nx = tile.x;
@@ -3049,42 +2967,28 @@ function createTorusTopology(cfg) {
3049
2967
  const v0 = tile.y / nV;
3050
2968
  const stepU = 1 / nU;
3051
2969
  const stepV = 1 / nV;
3052
- const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
2970
+ const dispLo = elevationRange ? elevationRange.min : 0;
2971
+ const dispHi = elevationRange ? elevationRange.max : 0;
3053
2972
  let pointCount = 0;
3054
- let sumX = 0;
3055
- let sumY = 0;
3056
- let sumZ = 0;
3057
2973
  for (let sj = 0; sj <= 2; sj++) {
3058
2974
  for (let si = 0; si <= 2; si++) {
3059
2975
  const u = u0 + si * stepU / 2;
3060
2976
  const v = v0 + sj * stepV / 2;
3061
- for (let di = 0; di < disps.length; di++) {
3062
- torusUVToPoint(u, v, majorRadius, minorRadius, disps[di], center, corner, invert);
2977
+ torusUVToPoint(u, v, majorRadius, minorRadius, dispLo, center, corner, invert);
2978
+ px[pointCount] = corner[0];
2979
+ py[pointCount] = corner[1];
2980
+ pz[pointCount] = corner[2];
2981
+ pointCount += 1;
2982
+ if (elevationRange) {
2983
+ torusUVToPoint(u, v, majorRadius, minorRadius, dispHi, center, corner, invert);
3063
2984
  px[pointCount] = corner[0];
3064
2985
  py[pointCount] = corner[1];
3065
2986
  pz[pointCount] = corner[2];
3066
- sumX += corner[0];
3067
- sumY += corner[1];
3068
- sumZ += corner[2];
3069
2987
  pointCount += 1;
3070
2988
  }
3071
2989
  }
3072
2990
  }
3073
- const cX = sumX / pointCount;
3074
- const cY = sumY / pointCount;
3075
- const cZ = sumZ / pointCount;
3076
- let maxDistSq = 0;
3077
- for (let i = 0; i < pointCount; i++) {
3078
- const dx = px[i] - cX;
3079
- const dy = py[i] - cY;
3080
- const dz = pz[i] - cZ;
3081
- const dSq = dx * dx + dy * dy + dz * dz;
3082
- if (dSq > maxDistSq) maxDistSq = dSq;
3083
- }
3084
- out.cx = cX - cameraOrigin.x;
3085
- out.cy = cY - cameraOrigin.y;
3086
- out.cz = cZ - cameraOrigin.z;
3087
- out.r = Math.sqrt(maxDistSq);
2991
+ boundingSphereFromPoints(px, py, pz, pointCount, cameraOrigin, out);
3088
2992
  },
3089
2993
  rootTiles(_cameraOrigin, out) {
3090
2994
  let count = 0;
@@ -3648,6 +3552,9 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3648
3552
  get hasSurface() {
3649
3553
  return surfaceOps !== null;
3650
3554
  },
3555
+ setSurfaceOps(nextSurfaceOps) {
3556
+ surfaceOps = nextSurfaceOps;
3557
+ },
3651
3558
  updateConfig(nextConfig) {
3652
3559
  config = nextConfig;
3653
3560
  shape.edgeVertexCount = config.innerTileSegments + 3;
@@ -3799,7 +3706,7 @@ function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
3799
3706
  }
3800
3707
 
3801
3708
  const WGSIZE = 64;
3802
- function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
3709
+ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode, edgeVertexCount) {
3803
3710
  const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
3804
3711
  return tsl.Fn(() => {
3805
3712
  const sharedMin = tsl.workgroupArray("float", WGSIZE);
@@ -3811,10 +3718,17 @@ function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
3811
3718
  const end = tsl.min(start.add(tsl.int(elemsPerThread)), tsl.int(verticesPerNode));
3812
3719
  const localMin = tsl.float(1e10).toVar("localMin");
3813
3720
  const localMax = tsl.float(-1e10).toVar("localMax");
3721
+ const edge = tsl.int(edgeVertexCount);
3722
+ const lastEdge = tsl.int(edgeVertexCount - 1);
3814
3723
  tsl.Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
3815
- const h = elevationFieldNode.element(baseOffset.add(i));
3816
- localMin.assign(tsl.min(localMin, h));
3817
- localMax.assign(tsl.max(localMax, h));
3724
+ const ix = tsl.int(i).mod(edge);
3725
+ const iy = tsl.int(i).div(edge);
3726
+ const isSkirt = ix.equal(tsl.int(0)).or(ix.equal(lastEdge)).or(iy.equal(tsl.int(0))).or(iy.equal(lastEdge));
3727
+ tsl.If(isSkirt.not(), () => {
3728
+ const h = elevationFieldNode.element(baseOffset.add(i));
3729
+ localMin.assign(tsl.min(localMin, h));
3730
+ localMax.assign(tsl.max(localMax, h));
3731
+ });
3818
3732
  });
3819
3733
  sharedMin.element(tid).assign(localMin);
3820
3734
  sharedMax.element(tid).assign(localMax);
@@ -3844,7 +3758,12 @@ const tileBoundsContextTask = work.task((get, work) => {
3844
3758
  "tileBounds"
3845
3759
  );
3846
3760
  const verticesPerNode = edgeVertexCount * edgeVertexCount;
3847
- const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
3761
+ const kernel = buildReductionKernel(
3762
+ elevationFieldContext.node,
3763
+ node,
3764
+ verticesPerNode,
3765
+ edgeVertexCount
3766
+ );
3848
3767
  return { data, attribute, node, kernel };
3849
3768
  });
3850
3769
  }).displayName("tileBoundsContextTask");
@@ -3874,6 +3793,7 @@ const terrainQueryTask = work.task((get, work) => {
3874
3793
  const projection = topologyValue.projection;
3875
3794
  return work((prev) => {
3876
3795
  const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projection.kind}`;
3796
+ const resolvedRadius = projection.radius ?? radiusValue;
3877
3797
  const configValues = {
3878
3798
  rootSize: rootSizeValue,
3879
3799
  originX: originValue.x,
@@ -3882,7 +3802,7 @@ const terrainQueryTask = work.task((get, work) => {
3882
3802
  innerTileSegments: innerTileSegmentsValue,
3883
3803
  elevationScale: elevationScaleValue,
3884
3804
  maxLevel: maxLevelValue,
3885
- radius: topologyValue.radius ?? radiusValue,
3805
+ radius: resolvedRadius,
3886
3806
  baseU: projection.baseResolution?.u ?? 1,
3887
3807
  baseV: projection.baseResolution?.v ?? 1
3888
3808
  };
@@ -3897,9 +3817,15 @@ const terrainQueryTask = work.task((get, work) => {
3897
3817
  query = runtime.query;
3898
3818
  surfaceQuery = runtime.surfaceQuery;
3899
3819
  sphereQuery = runtime.sphereQuery;
3820
+ } else if (prev?.projection !== projection) {
3821
+ cache.setSurfaceOps(projection.cpu.createSurfaceOps());
3822
+ const runtime = projection.cpu.createRuntimeQueries(cache);
3823
+ query = runtime.query;
3824
+ surfaceQuery = runtime.surfaceQuery;
3825
+ sphereQuery = runtime.sphereQuery;
3900
3826
  }
3901
3827
  cache.updateConfig(configValues);
3902
- return { cache, query, surfaceQuery, sphereQuery, shapeKey };
3828
+ return { cache, query, surfaceQuery, sphereQuery, shapeKey, projection };
3903
3829
  });
3904
3830
  }).displayName("terrainQueryTask");
3905
3831
  const terrainReadbackTask = work.task(
@@ -3946,27 +3872,19 @@ const quadtreeConfigTask = work.task((get, work) => {
3946
3872
  const quadtreeUpdateTask = work.task((get, work) => {
3947
3873
  const quadtreeConfig = get(quadtreeConfigTask);
3948
3874
  const quadtreeUpdateConfig = get(quadtreeUpdate);
3949
- const { query: terrainQuery, surfaceQuery, cache } = get(terrainQueryTask);
3875
+ const { cache } = get(terrainQueryTask);
3950
3876
  const elevationScaleValue = get(elevationScale);
3951
3877
  let outLeaves = void 0;
3952
- const cameraPosition = new three.Vector3();
3953
3878
  const elevationRangeScratch = { min: 0, max: 0 };
3954
- return work(() => {
3955
- const cam = quadtreeUpdateConfig.cameraOrigin;
3956
- if (surfaceQuery) {
3957
- cameraPosition.set(cam.x, cam.y, cam.z);
3958
- quadtreeUpdateConfig.elevationAtCameraXZ = surfaceQuery.getElevationByPosition(cameraPosition) ?? 0;
3959
- } else {
3960
- quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
3879
+ quadtreeUpdateConfig.tileElevationRange = (tile, out) => {
3880
+ if (!cache.getTileElevationRange(tile.space, tile.level, tile.x, tile.y, elevationRangeScratch)) {
3881
+ return false;
3961
3882
  }
3962
- quadtreeUpdateConfig.tileElevationRange = (space, level, x, y, out) => {
3963
- if (!cache.getTileElevationRange(space, level, x, y, elevationRangeScratch)) {
3964
- return false;
3965
- }
3966
- out.min = elevationRangeScratch.min * elevationScaleValue;
3967
- out.max = elevationRangeScratch.max * elevationScaleValue;
3968
- return true;
3969
- };
3883
+ out.min = elevationRangeScratch.min * elevationScaleValue;
3884
+ out.max = elevationRangeScratch.max * elevationScaleValue;
3885
+ return true;
3886
+ };
3887
+ return work(() => {
3970
3888
  outLeaves = update(
3971
3889
  quadtreeConfig.state,
3972
3890
  quadtreeConfig.topology,