@hello-terrain/react 0.0.0-alpha.10 → 0.0.0-alpha.11

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
@@ -21,10 +21,13 @@ function useTerrainContext() {
21
21
 
22
22
  function createSyncTerrainRuntimeTask(runtime) {
23
23
  return work.task((get, work) => {
24
- const query = get(three.terrainTasks.terrainQuery).query;
24
+ const queryContext = get(three.terrainTasks.terrainQuery);
25
+ const query = queryContext.query;
26
+ const sphereQuery = queryContext.sphereQuery;
25
27
  const raycast = get(three.terrainTasks.terrainRaycast);
26
28
  return work(() => {
27
29
  runtime.query = query;
30
+ runtime.sphereQuery = sphereQuery;
28
31
  runtime.raycast = raycast;
29
32
  return runtime;
30
33
  });
@@ -73,8 +76,9 @@ function useTerrainParams(graph, options) {
73
76
  innerTileSegments: nextInnerTileSegments,
74
77
  skirtScale: nextSkirtScale,
75
78
  elevationScale: nextElevationScale,
79
+ radius: nextRadius,
76
80
  elevation,
77
- surface: nextSurface,
81
+ topology: nextTopology,
78
82
  terrainFieldFilter: nextTerrainFieldFilter
79
83
  } = options;
80
84
  react.useLayoutEffect(() => {
@@ -132,9 +136,16 @@ function useTerrainParams(graph, options) {
132
136
  resetOrSet(
133
137
  graph,
134
138
  ownedParamIds,
135
- three.surface,
136
- nextSurface,
137
- () => nextSurface
139
+ three.radius,
140
+ nextRadius,
141
+ () => nextRadius
142
+ );
143
+ resetOrSet(
144
+ graph,
145
+ ownedParamIds,
146
+ three.topology,
147
+ nextTopology,
148
+ () => nextTopology
138
149
  );
139
150
  resetOrSet(
140
151
  graph,
@@ -161,8 +172,9 @@ function useTerrainParams(graph, options) {
161
172
  nextInnerTileSegments,
162
173
  nextSkirtScale,
163
174
  nextElevationScale,
175
+ nextRadius,
164
176
  elevation,
165
- nextSurface,
177
+ nextTopology,
166
178
  nextTerrainFieldFilter
167
179
  ]);
168
180
  }
@@ -316,6 +328,7 @@ function useTerrain(options = {}) {
316
328
  const runtime = react.useMemo(
317
329
  () => ({
318
330
  query: null,
331
+ sphereQuery: null,
319
332
  raycast: null
320
333
  }),
321
334
  []
@@ -328,44 +341,27 @@ function useTerrain(options = {}) {
328
341
  const [terrainNodes, setTerrainNodes] = react.useState(terrainNodesRef.current);
329
342
  const readyRef = react.useRef(false);
330
343
  const [ready, setReady] = react.useState(false);
331
- const emitTerrainNodes = react.useCallback(
332
- (nextTerrainNodes) => {
333
- if (!isMountedRef.current) return;
334
- setTerrainNodes((prevTerrainNodes) => {
335
- if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
336
- terrainNodesRef.current = prevTerrainNodes;
337
- return prevTerrainNodes;
338
- }
339
- terrainNodesRef.current = nextTerrainNodes;
340
- return nextTerrainNodes;
341
- });
342
- },
343
- []
344
- );
345
- const getTerrainNodes = react.useCallback(
346
- () => terrainNodesRef.current,
347
- []
348
- );
344
+ const emitTerrainNodes = react.useCallback((nextTerrainNodes) => {
345
+ if (!isMountedRef.current) return;
346
+ setTerrainNodes((prevTerrainNodes) => {
347
+ if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
348
+ terrainNodesRef.current = prevTerrainNodes;
349
+ return prevTerrainNodes;
350
+ }
351
+ terrainNodesRef.current = nextTerrainNodes;
352
+ return nextTerrainNodes;
353
+ });
354
+ }, []);
355
+ const getTerrainNodes = react.useCallback(() => terrainNodesRef.current, []);
349
356
  const emitReady = react.useCallback((nextReady) => {
350
357
  if (!isMountedRef.current) return;
351
358
  readyRef.current = nextReady;
352
359
  setReady((prevReady) => prevReady === nextReady ? prevReady : nextReady);
353
360
  }, []);
354
- const getReady = react.useCallback(
355
- () => readyRef.current,
356
- []
357
- );
358
- const syncTerrainRuntimeTask = react.useMemo(
359
- () => createSyncTerrainRuntimeTask(runtime),
360
- [runtime]
361
- );
361
+ const getReady = react.useCallback(() => readyRef.current, []);
362
+ const syncTerrainRuntimeTask = react.useMemo(() => createSyncTerrainRuntimeTask(runtime), [runtime]);
362
363
  const syncTerrainNodesTask = react.useMemo(
363
- () => createSyncTerrainNodesTask(
364
- getTerrainNodes,
365
- emitTerrainNodes,
366
- getReady,
367
- emitReady
368
- ),
364
+ () => createSyncTerrainNodesTask(getTerrainNodes, emitTerrainNodes, getReady, emitReady),
369
365
  [emitReady, emitTerrainNodes, getReady, getTerrainNodes]
370
366
  );
371
367
  const graph = react.useMemo(() => {
@@ -377,19 +373,17 @@ function useTerrain(options = {}) {
377
373
  nextGraph.add(syncTerrainNodesTask);
378
374
  return nextGraph;
379
375
  }, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
380
- const runnerTargets = react.useMemo(
381
- () => {
382
- const userTasks = options.tasks ?? [];
383
- return [
384
- ...userTasks,
385
- three.terrainTasks.executeCompute,
386
- three.terrainTasks.terrainReadback,
387
- syncTerrainRuntimeTask,
388
- syncTerrainNodesTask
389
- ];
390
- },
391
- [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]
392
- );
376
+ const runnerTargets = react.useMemo(() => {
377
+ const userTasks = options.tasks ?? [];
378
+ return [
379
+ ...userTasks,
380
+ three.terrainTasks.executeCompute,
381
+ three.terrainTasks.terrainReadback,
382
+ three.terrainTasks.gpuSpatialIndexUpload,
383
+ syncTerrainRuntimeTask,
384
+ syncTerrainNodesTask
385
+ ];
386
+ }, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
393
387
  const stopTerrainRunner = useTerrainRunner({
394
388
  graph,
395
389
  targets: runnerTargets,
@@ -423,31 +417,34 @@ function useTerrain(options = {}) {
423
417
  setReady(false);
424
418
  }, [graph, runtime]);
425
419
  useTerrainParams(graph, options);
420
+ const topology = options.topology ?? null;
426
421
  return react.useMemo(
427
422
  () => ({
428
423
  graph,
429
424
  tasks: three.terrainTasks,
430
425
  runtime,
431
426
  ready,
427
+ topology,
432
428
  ...terrainNodes
433
429
  }),
434
- [graph, ready, runtime, terrainNodes]
430
+ [graph, ready, runtime, topology, terrainNodes]
435
431
  );
436
432
  }
437
433
 
438
- function useTerrainMesh(innerTileSegments, maxNodes) {
434
+ function useTerrainMesh(innerTileSegments, maxNodes, flipWinding) {
439
435
  const [mesh] = react.useState(
440
436
  () => new three.TerrainMesh({
441
- innerTileSegments: innerTileSegments ?? 13,
442
- maxNodes: maxNodes ?? 1024
437
+ ...innerTileSegments !== void 0 ? { innerTileSegments } : {},
438
+ maxNodes: maxNodes ?? 1024,
439
+ flipWinding
443
440
  })
444
441
  );
445
- react.useEffect(() => {
446
- mesh.innerTileSegments = innerTileSegments ?? 13;
447
- }, [mesh, innerTileSegments]);
448
442
  react.useEffect(() => {
449
443
  mesh.maxNodes = maxNodes ?? 1024;
450
444
  }, [mesh, maxNodes]);
445
+ react.useEffect(() => {
446
+ mesh.flipWinding = flipWinding;
447
+ }, [mesh, flipWinding]);
451
448
  react.useEffect(() => {
452
449
  return () => {
453
450
  mesh.geometry.dispose();
@@ -461,6 +458,13 @@ function syncTerrainMesh(mesh, terrain) {
461
458
  mesh.count = leaves.count;
462
459
  mesh.instanceMatrix.needsUpdate = true;
463
460
  }
461
+ const uniforms = terrain.graph.peek(three.terrainTasks.updateUniforms);
462
+ if (uniforms) {
463
+ const segments = uniforms.uInnerTileSegments.value;
464
+ if (typeof segments === "number") {
465
+ mesh.innerTileSegments = segments;
466
+ }
467
+ }
464
468
  const raycast = terrain.runtime.raycast;
465
469
  if (mesh.terrainRaycast !== raycast) {
466
470
  mesh.terrainRaycast = raycast;
@@ -481,25 +485,18 @@ function TerrainWithHandle({
481
485
  maxNodes,
482
486
  ...primitiveProps
483
487
  }) {
484
- const mesh = useTerrainMesh(innerTileSegments, maxNodes);
488
+ const flipWinding = (terrain.topology?.projection ?? "flat") === "cubeSphere";
489
+ const mesh = useTerrainMesh(innerTileSegments, maxNodes, flipWinding);
485
490
  const { visible: primitiveVisible = true, ...restPrimitiveProps } = primitiveProps;
486
491
  fiber.useFrame(() => {
487
492
  syncTerrainMesh(mesh, terrain);
488
493
  });
489
- return /* @__PURE__ */ jsxRuntime.jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsxRuntime.jsx(
490
- "primitive",
491
- {
492
- object: mesh,
493
- visible: terrain.ready && primitiveVisible,
494
- ...restPrimitiveProps,
495
- children: terrain.ready ? attachTerrainMaterial(
496
- children({
497
- positionNode: terrain.positionNode
498
- }),
499
- terrain
500
- ) : null
501
- }
502
- ) });
494
+ return /* @__PURE__ */ jsxRuntime.jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsxRuntime.jsx("primitive", { object: mesh, visible: terrain.ready && primitiveVisible, ...restPrimitiveProps, children: terrain.ready ? attachTerrainMaterial(
495
+ children({
496
+ positionNode: terrain.positionNode
497
+ }),
498
+ terrain
499
+ ) : null }) });
503
500
  }
504
501
  function InternalTerrain(props) {
505
502
  const {
@@ -510,8 +507,9 @@ function InternalTerrain(props) {
510
507
  innerTileSegments,
511
508
  skirtScale,
512
509
  elevationScale,
510
+ radius,
513
511
  elevation,
514
- surface,
512
+ topology,
515
513
  terrainFieldFilter,
516
514
  getCameraOrigin,
517
515
  cameraHysteresis,
@@ -526,8 +524,9 @@ function InternalTerrain(props) {
526
524
  innerTileSegments,
527
525
  skirtScale,
528
526
  elevationScale,
527
+ radius,
529
528
  elevation,
530
- surface,
529
+ topology,
531
530
  terrainFieldFilter,
532
531
  getCameraOrigin,
533
532
  cameraHysteresis,
@@ -554,8 +553,9 @@ function Terrain({
554
553
  innerTileSegments,
555
554
  skirtScale,
556
555
  elevationScale,
556
+ radius,
557
557
  elevation,
558
- surface,
558
+ topology,
559
559
  terrainFieldFilter,
560
560
  getCameraOrigin,
561
561
  cameraHysteresis,
@@ -584,8 +584,9 @@ function Terrain({
584
584
  innerTileSegments,
585
585
  skirtScale,
586
586
  elevationScale,
587
+ radius,
587
588
  elevation,
588
- surface,
589
+ topology,
589
590
  terrainFieldFilter,
590
591
  getCameraOrigin,
591
592
  cameraHysteresis,
package/dist/index.d.cts CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ElevationCallback, Surface, TerrainGraph, TerrainTasks, TerrainQuery, TerrainRaycast } from '@hello-terrain/three';
2
+ import { ElevationCallback, Topology, TerrainGraph, TerrainTasks, TerrainQuery, TerrainSphereQuery, TerrainRaycast } from '@hello-terrain/three';
3
+ export { TerrainQuery, TerrainRaycast, TerrainRaycastResult, TerrainSample, TerrainSphereQuery, TerrainSurfaceSample, TerrainSurfaceSampleBatch, TerrainTile } from '@hello-terrain/three';
3
4
  import { Task } from '@hello-terrain/work';
4
5
  import { ThreeElements, RootState } from '@react-three/fiber';
5
6
  import { ReactNode } from 'react';
6
- import { WebGPURenderer } from 'three/webgpu';
7
7
  import { ShaderCallNodeInternal } from 'three/src/nodes/TSL.js';
8
+ import { WebGPURenderer } from 'three/webgpu';
8
9
 
9
10
  type TerrainVector3Like = {
10
11
  x: number;
@@ -19,6 +20,8 @@ interface TerrainNodes {
19
20
  }
20
21
  interface TerrainRuntime {
21
22
  query: TerrainQuery | null;
23
+ /** Cube-sphere query; `null` unless the topology uses the cubeSphere projection. */
24
+ sphereQuery: TerrainSphereQuery | null;
22
25
  raycast: TerrainRaycast | null;
23
26
  }
24
27
  interface TerrainHandle extends TerrainNodes {
@@ -26,6 +29,7 @@ interface TerrainHandle extends TerrainNodes {
26
29
  tasks: TerrainTasks;
27
30
  runtime: TerrainRuntime;
28
31
  ready: boolean;
32
+ topology?: Topology | null;
29
33
  }
30
34
  interface TerrainOptions {
31
35
  rootSize?: number;
@@ -35,8 +39,9 @@ interface TerrainOptions {
35
39
  innerTileSegments?: number;
36
40
  skirtScale?: number;
37
41
  elevationScale?: number;
42
+ radius?: number;
38
43
  elevation?: ElevationCallback;
39
- surface?: Surface | null;
44
+ topology?: Topology | null;
40
45
  terrainFieldFilter?: "nearest" | "linear";
41
46
  getCameraOrigin?: (state: RootState) => TerrainVector3Like;
42
47
  cameraHysteresis?: number;
@@ -48,7 +53,7 @@ interface TerrainProps extends TerrainPrimitiveProps, TerrainOptions {
48
53
  children: (nodes: TerrainNodes) => ReactNode;
49
54
  }
50
55
 
51
- declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, elevation, surface, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
56
+ declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, radius, elevation, topology, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
52
57
 
53
58
  interface TerrainProviderProps {
54
59
  value: TerrainHandle;
package/dist/index.d.mts CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ElevationCallback, Surface, TerrainGraph, TerrainTasks, TerrainQuery, TerrainRaycast } from '@hello-terrain/three';
2
+ import { ElevationCallback, Topology, TerrainGraph, TerrainTasks, TerrainQuery, TerrainSphereQuery, TerrainRaycast } from '@hello-terrain/three';
3
+ export { TerrainQuery, TerrainRaycast, TerrainRaycastResult, TerrainSample, TerrainSphereQuery, TerrainSurfaceSample, TerrainSurfaceSampleBatch, TerrainTile } from '@hello-terrain/three';
3
4
  import { Task } from '@hello-terrain/work';
4
5
  import { ThreeElements, RootState } from '@react-three/fiber';
5
6
  import { ReactNode } from 'react';
6
- import { WebGPURenderer } from 'three/webgpu';
7
7
  import { ShaderCallNodeInternal } from 'three/src/nodes/TSL.js';
8
+ import { WebGPURenderer } from 'three/webgpu';
8
9
 
9
10
  type TerrainVector3Like = {
10
11
  x: number;
@@ -19,6 +20,8 @@ interface TerrainNodes {
19
20
  }
20
21
  interface TerrainRuntime {
21
22
  query: TerrainQuery | null;
23
+ /** Cube-sphere query; `null` unless the topology uses the cubeSphere projection. */
24
+ sphereQuery: TerrainSphereQuery | null;
22
25
  raycast: TerrainRaycast | null;
23
26
  }
24
27
  interface TerrainHandle extends TerrainNodes {
@@ -26,6 +29,7 @@ interface TerrainHandle extends TerrainNodes {
26
29
  tasks: TerrainTasks;
27
30
  runtime: TerrainRuntime;
28
31
  ready: boolean;
32
+ topology?: Topology | null;
29
33
  }
30
34
  interface TerrainOptions {
31
35
  rootSize?: number;
@@ -35,8 +39,9 @@ interface TerrainOptions {
35
39
  innerTileSegments?: number;
36
40
  skirtScale?: number;
37
41
  elevationScale?: number;
42
+ radius?: number;
38
43
  elevation?: ElevationCallback;
39
- surface?: Surface | null;
44
+ topology?: Topology | null;
40
45
  terrainFieldFilter?: "nearest" | "linear";
41
46
  getCameraOrigin?: (state: RootState) => TerrainVector3Like;
42
47
  cameraHysteresis?: number;
@@ -48,7 +53,7 @@ interface TerrainProps extends TerrainPrimitiveProps, TerrainOptions {
48
53
  children: (nodes: TerrainNodes) => ReactNode;
49
54
  }
50
55
 
51
- declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, elevation, surface, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
56
+ declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, radius, elevation, topology, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
52
57
 
53
58
  interface TerrainProviderProps {
54
59
  value: TerrainHandle;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ElevationCallback, Surface, TerrainGraph, TerrainTasks, TerrainQuery, TerrainRaycast } from '@hello-terrain/three';
2
+ import { ElevationCallback, Topology, TerrainGraph, TerrainTasks, TerrainQuery, TerrainSphereQuery, TerrainRaycast } from '@hello-terrain/three';
3
+ export { TerrainQuery, TerrainRaycast, TerrainRaycastResult, TerrainSample, TerrainSphereQuery, TerrainSurfaceSample, TerrainSurfaceSampleBatch, TerrainTile } from '@hello-terrain/three';
3
4
  import { Task } from '@hello-terrain/work';
4
5
  import { ThreeElements, RootState } from '@react-three/fiber';
5
6
  import { ReactNode } from 'react';
6
- import { WebGPURenderer } from 'three/webgpu';
7
7
  import { ShaderCallNodeInternal } from 'three/src/nodes/TSL.js';
8
+ import { WebGPURenderer } from 'three/webgpu';
8
9
 
9
10
  type TerrainVector3Like = {
10
11
  x: number;
@@ -19,6 +20,8 @@ interface TerrainNodes {
19
20
  }
20
21
  interface TerrainRuntime {
21
22
  query: TerrainQuery | null;
23
+ /** Cube-sphere query; `null` unless the topology uses the cubeSphere projection. */
24
+ sphereQuery: TerrainSphereQuery | null;
22
25
  raycast: TerrainRaycast | null;
23
26
  }
24
27
  interface TerrainHandle extends TerrainNodes {
@@ -26,6 +29,7 @@ interface TerrainHandle extends TerrainNodes {
26
29
  tasks: TerrainTasks;
27
30
  runtime: TerrainRuntime;
28
31
  ready: boolean;
32
+ topology?: Topology | null;
29
33
  }
30
34
  interface TerrainOptions {
31
35
  rootSize?: number;
@@ -35,8 +39,9 @@ interface TerrainOptions {
35
39
  innerTileSegments?: number;
36
40
  skirtScale?: number;
37
41
  elevationScale?: number;
42
+ radius?: number;
38
43
  elevation?: ElevationCallback;
39
- surface?: Surface | null;
44
+ topology?: Topology | null;
40
45
  terrainFieldFilter?: "nearest" | "linear";
41
46
  getCameraOrigin?: (state: RootState) => TerrainVector3Like;
42
47
  cameraHysteresis?: number;
@@ -48,7 +53,7 @@ interface TerrainProps extends TerrainPrimitiveProps, TerrainOptions {
48
53
  children: (nodes: TerrainNodes) => ReactNode;
49
54
  }
50
55
 
51
- declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, elevation, surface, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
56
+ declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, radius, elevation, topology, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
52
57
 
53
58
  interface TerrainProviderProps {
54
59
  value: TerrainHandle;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { terrainTasks, elevationFn, rootSize, origin, maxLevel, maxNodes, innerTileSegments, skirtScale, elevationScale, surface, terrainFieldFilter, quadtreeUpdate, terrainGraph, TerrainMesh } from '@hello-terrain/three';
2
+ import { terrainTasks, elevationFn, rootSize, origin, maxLevel, maxNodes, innerTileSegments, skirtScale, elevationScale, radius, topology, terrainFieldFilter, quadtreeUpdate, terrainGraph, TerrainMesh } from '@hello-terrain/three';
3
3
  import { useFrame } from '@react-three/fiber';
4
4
  import { createContext, useContext, useRef, useLayoutEffect, useCallback, useMemo, useState, useEffect, isValidElement, cloneElement } from 'react';
5
5
  import { task } from '@hello-terrain/work';
@@ -19,10 +19,13 @@ function useTerrainContext() {
19
19
 
20
20
  function createSyncTerrainRuntimeTask(runtime) {
21
21
  return task((get, work) => {
22
- const query = get(terrainTasks.terrainQuery).query;
22
+ const queryContext = get(terrainTasks.terrainQuery);
23
+ const query = queryContext.query;
24
+ const sphereQuery = queryContext.sphereQuery;
23
25
  const raycast = get(terrainTasks.terrainRaycast);
24
26
  return work(() => {
25
27
  runtime.query = query;
28
+ runtime.sphereQuery = sphereQuery;
26
29
  runtime.raycast = raycast;
27
30
  return runtime;
28
31
  });
@@ -71,8 +74,9 @@ function useTerrainParams(graph, options) {
71
74
  innerTileSegments: nextInnerTileSegments,
72
75
  skirtScale: nextSkirtScale,
73
76
  elevationScale: nextElevationScale,
77
+ radius: nextRadius,
74
78
  elevation,
75
- surface: nextSurface,
79
+ topology: nextTopology,
76
80
  terrainFieldFilter: nextTerrainFieldFilter
77
81
  } = options;
78
82
  useLayoutEffect(() => {
@@ -130,9 +134,16 @@ function useTerrainParams(graph, options) {
130
134
  resetOrSet(
131
135
  graph,
132
136
  ownedParamIds,
133
- surface,
134
- nextSurface,
135
- () => nextSurface
137
+ radius,
138
+ nextRadius,
139
+ () => nextRadius
140
+ );
141
+ resetOrSet(
142
+ graph,
143
+ ownedParamIds,
144
+ topology,
145
+ nextTopology,
146
+ () => nextTopology
136
147
  );
137
148
  resetOrSet(
138
149
  graph,
@@ -159,8 +170,9 @@ function useTerrainParams(graph, options) {
159
170
  nextInnerTileSegments,
160
171
  nextSkirtScale,
161
172
  nextElevationScale,
173
+ nextRadius,
162
174
  elevation,
163
- nextSurface,
175
+ nextTopology,
164
176
  nextTerrainFieldFilter
165
177
  ]);
166
178
  }
@@ -314,6 +326,7 @@ function useTerrain(options = {}) {
314
326
  const runtime = useMemo(
315
327
  () => ({
316
328
  query: null,
329
+ sphereQuery: null,
317
330
  raycast: null
318
331
  }),
319
332
  []
@@ -326,44 +339,27 @@ function useTerrain(options = {}) {
326
339
  const [terrainNodes, setTerrainNodes] = useState(terrainNodesRef.current);
327
340
  const readyRef = useRef(false);
328
341
  const [ready, setReady] = useState(false);
329
- const emitTerrainNodes = useCallback(
330
- (nextTerrainNodes) => {
331
- if (!isMountedRef.current) return;
332
- setTerrainNodes((prevTerrainNodes) => {
333
- if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
334
- terrainNodesRef.current = prevTerrainNodes;
335
- return prevTerrainNodes;
336
- }
337
- terrainNodesRef.current = nextTerrainNodes;
338
- return nextTerrainNodes;
339
- });
340
- },
341
- []
342
- );
343
- const getTerrainNodes = useCallback(
344
- () => terrainNodesRef.current,
345
- []
346
- );
342
+ const emitTerrainNodes = useCallback((nextTerrainNodes) => {
343
+ if (!isMountedRef.current) return;
344
+ setTerrainNodes((prevTerrainNodes) => {
345
+ if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
346
+ terrainNodesRef.current = prevTerrainNodes;
347
+ return prevTerrainNodes;
348
+ }
349
+ terrainNodesRef.current = nextTerrainNodes;
350
+ return nextTerrainNodes;
351
+ });
352
+ }, []);
353
+ const getTerrainNodes = useCallback(() => terrainNodesRef.current, []);
347
354
  const emitReady = useCallback((nextReady) => {
348
355
  if (!isMountedRef.current) return;
349
356
  readyRef.current = nextReady;
350
357
  setReady((prevReady) => prevReady === nextReady ? prevReady : nextReady);
351
358
  }, []);
352
- const getReady = useCallback(
353
- () => readyRef.current,
354
- []
355
- );
356
- const syncTerrainRuntimeTask = useMemo(
357
- () => createSyncTerrainRuntimeTask(runtime),
358
- [runtime]
359
- );
359
+ const getReady = useCallback(() => readyRef.current, []);
360
+ const syncTerrainRuntimeTask = useMemo(() => createSyncTerrainRuntimeTask(runtime), [runtime]);
360
361
  const syncTerrainNodesTask = useMemo(
361
- () => createSyncTerrainNodesTask(
362
- getTerrainNodes,
363
- emitTerrainNodes,
364
- getReady,
365
- emitReady
366
- ),
362
+ () => createSyncTerrainNodesTask(getTerrainNodes, emitTerrainNodes, getReady, emitReady),
367
363
  [emitReady, emitTerrainNodes, getReady, getTerrainNodes]
368
364
  );
369
365
  const graph = useMemo(() => {
@@ -375,19 +371,17 @@ function useTerrain(options = {}) {
375
371
  nextGraph.add(syncTerrainNodesTask);
376
372
  return nextGraph;
377
373
  }, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
378
- const runnerTargets = useMemo(
379
- () => {
380
- const userTasks = options.tasks ?? [];
381
- return [
382
- ...userTasks,
383
- terrainTasks.executeCompute,
384
- terrainTasks.terrainReadback,
385
- syncTerrainRuntimeTask,
386
- syncTerrainNodesTask
387
- ];
388
- },
389
- [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]
390
- );
374
+ const runnerTargets = useMemo(() => {
375
+ const userTasks = options.tasks ?? [];
376
+ return [
377
+ ...userTasks,
378
+ terrainTasks.executeCompute,
379
+ terrainTasks.terrainReadback,
380
+ terrainTasks.gpuSpatialIndexUpload,
381
+ syncTerrainRuntimeTask,
382
+ syncTerrainNodesTask
383
+ ];
384
+ }, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
391
385
  const stopTerrainRunner = useTerrainRunner({
392
386
  graph,
393
387
  targets: runnerTargets,
@@ -421,31 +415,34 @@ function useTerrain(options = {}) {
421
415
  setReady(false);
422
416
  }, [graph, runtime]);
423
417
  useTerrainParams(graph, options);
418
+ const topology = options.topology ?? null;
424
419
  return useMemo(
425
420
  () => ({
426
421
  graph,
427
422
  tasks: terrainTasks,
428
423
  runtime,
429
424
  ready,
425
+ topology,
430
426
  ...terrainNodes
431
427
  }),
432
- [graph, ready, runtime, terrainNodes]
428
+ [graph, ready, runtime, topology, terrainNodes]
433
429
  );
434
430
  }
435
431
 
436
- function useTerrainMesh(innerTileSegments, maxNodes) {
432
+ function useTerrainMesh(innerTileSegments, maxNodes, flipWinding) {
437
433
  const [mesh] = useState(
438
434
  () => new TerrainMesh({
439
- innerTileSegments: innerTileSegments ?? 13,
440
- maxNodes: maxNodes ?? 1024
435
+ ...innerTileSegments !== void 0 ? { innerTileSegments } : {},
436
+ maxNodes: maxNodes ?? 1024,
437
+ flipWinding
441
438
  })
442
439
  );
443
- useEffect(() => {
444
- mesh.innerTileSegments = innerTileSegments ?? 13;
445
- }, [mesh, innerTileSegments]);
446
440
  useEffect(() => {
447
441
  mesh.maxNodes = maxNodes ?? 1024;
448
442
  }, [mesh, maxNodes]);
443
+ useEffect(() => {
444
+ mesh.flipWinding = flipWinding;
445
+ }, [mesh, flipWinding]);
449
446
  useEffect(() => {
450
447
  return () => {
451
448
  mesh.geometry.dispose();
@@ -459,6 +456,13 @@ function syncTerrainMesh(mesh, terrain) {
459
456
  mesh.count = leaves.count;
460
457
  mesh.instanceMatrix.needsUpdate = true;
461
458
  }
459
+ const uniforms = terrain.graph.peek(terrainTasks.updateUniforms);
460
+ if (uniforms) {
461
+ const segments = uniforms.uInnerTileSegments.value;
462
+ if (typeof segments === "number") {
463
+ mesh.innerTileSegments = segments;
464
+ }
465
+ }
462
466
  const raycast = terrain.runtime.raycast;
463
467
  if (mesh.terrainRaycast !== raycast) {
464
468
  mesh.terrainRaycast = raycast;
@@ -479,25 +483,18 @@ function TerrainWithHandle({
479
483
  maxNodes,
480
484
  ...primitiveProps
481
485
  }) {
482
- const mesh = useTerrainMesh(innerTileSegments, maxNodes);
486
+ const flipWinding = (terrain.topology?.projection ?? "flat") === "cubeSphere";
487
+ const mesh = useTerrainMesh(innerTileSegments, maxNodes, flipWinding);
483
488
  const { visible: primitiveVisible = true, ...restPrimitiveProps } = primitiveProps;
484
489
  useFrame(() => {
485
490
  syncTerrainMesh(mesh, terrain);
486
491
  });
487
- return /* @__PURE__ */ jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsx(
488
- "primitive",
489
- {
490
- object: mesh,
491
- visible: terrain.ready && primitiveVisible,
492
- ...restPrimitiveProps,
493
- children: terrain.ready ? attachTerrainMaterial(
494
- children({
495
- positionNode: terrain.positionNode
496
- }),
497
- terrain
498
- ) : null
499
- }
500
- ) });
492
+ return /* @__PURE__ */ jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsx("primitive", { object: mesh, visible: terrain.ready && primitiveVisible, ...restPrimitiveProps, children: terrain.ready ? attachTerrainMaterial(
493
+ children({
494
+ positionNode: terrain.positionNode
495
+ }),
496
+ terrain
497
+ ) : null }) });
501
498
  }
502
499
  function InternalTerrain(props) {
503
500
  const {
@@ -508,8 +505,9 @@ function InternalTerrain(props) {
508
505
  innerTileSegments,
509
506
  skirtScale,
510
507
  elevationScale,
508
+ radius,
511
509
  elevation,
512
- surface,
510
+ topology,
513
511
  terrainFieldFilter,
514
512
  getCameraOrigin,
515
513
  cameraHysteresis,
@@ -524,8 +522,9 @@ function InternalTerrain(props) {
524
522
  innerTileSegments,
525
523
  skirtScale,
526
524
  elevationScale,
525
+ radius,
527
526
  elevation,
528
- surface,
527
+ topology,
529
528
  terrainFieldFilter,
530
529
  getCameraOrigin,
531
530
  cameraHysteresis,
@@ -552,8 +551,9 @@ function Terrain({
552
551
  innerTileSegments,
553
552
  skirtScale,
554
553
  elevationScale,
554
+ radius,
555
555
  elevation,
556
- surface,
556
+ topology,
557
557
  terrainFieldFilter,
558
558
  getCameraOrigin,
559
559
  cameraHysteresis,
@@ -582,8 +582,9 @@ function Terrain({
582
582
  innerTileSegments,
583
583
  skirtScale,
584
584
  elevationScale,
585
+ radius,
585
586
  elevation,
586
- surface,
587
+ topology,
587
588
  terrainFieldFilter,
588
589
  getCameraOrigin,
589
590
  cameraHysteresis,
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/kenjinp/hello-terrain.git"
8
8
  },
9
- "version": "0.0.0-alpha.10",
9
+ "version": "0.0.0-alpha.11",
10
10
  "type": "module",
11
11
  "main": "./dist/index.mjs",
12
12
  "module": "./dist/index.mjs",
@@ -22,8 +22,8 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@hello-terrain/three": "0.0.0-alpha.10",
26
- "@hello-terrain/work": "0.3.0"
25
+ "@hello-terrain/work": "0.3.0",
26
+ "@hello-terrain/three": "0.0.0-alpha.11"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@testing-library/dom": "^10.4.0",
@@ -34,9 +34,9 @@
34
34
  "jsdom": "^26.0.0",
35
35
  "unbuild": "^3.5.0",
36
36
  "vitest": "^4.0.17",
37
+ "@config/oxlint": "0.1.0",
37
38
  "@config/oxfmt": "0.1.0",
38
- "@config/typescript": "0.1.0",
39
- "@config/oxlint": "0.1.0"
39
+ "@config/typescript": "0.1.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@react-three/fiber": "^8.0.0 || ^9.0.0",
@@ -51,6 +51,7 @@
51
51
  "build": "unbuild",
52
52
  "release": "pnpm run build && pnpm publish --access=public",
53
53
  "test": "vitest",
54
+ "typecheck": "tsc -p tsconfig.json --noEmit --composite false",
54
55
  "lint": "oxlint -c node_modules/@config/oxlint/react.json",
55
56
  "format": "oxfmt -c node_modules/@config/oxfmt/base.json --write ."
56
57
  }