@hello-terrain/react 0.0.0-alpha.10
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/README.md +85 -0
- package/dist/index.cjs +603 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.mts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.mjs +598 -0
- package/package.json +57 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
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';
|
|
3
|
+
import { useFrame } from '@react-three/fiber';
|
|
4
|
+
import { createContext, useContext, useRef, useLayoutEffect, useCallback, useMemo, useState, useEffect, isValidElement, cloneElement } from 'react';
|
|
5
|
+
import { task } from '@hello-terrain/work';
|
|
6
|
+
import { Vector3 } from 'three';
|
|
7
|
+
|
|
8
|
+
const TerrainContext = createContext(null);
|
|
9
|
+
function TerrainProvider({ value, children }) {
|
|
10
|
+
return /* @__PURE__ */ jsx(TerrainContext.Provider, { value, children });
|
|
11
|
+
}
|
|
12
|
+
function useTerrainContext() {
|
|
13
|
+
const value = useContext(TerrainContext);
|
|
14
|
+
if (!value) {
|
|
15
|
+
throw new Error("useTerrainContext must be used within a TerrainProvider");
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createSyncTerrainRuntimeTask(runtime) {
|
|
21
|
+
return task((get, work) => {
|
|
22
|
+
const query = get(terrainTasks.terrainQuery).query;
|
|
23
|
+
const raycast = get(terrainTasks.terrainRaycast);
|
|
24
|
+
return work(() => {
|
|
25
|
+
runtime.query = query;
|
|
26
|
+
runtime.raycast = raycast;
|
|
27
|
+
return runtime;
|
|
28
|
+
});
|
|
29
|
+
}).displayName("syncTerrainRuntimeTask").cache("none");
|
|
30
|
+
}
|
|
31
|
+
function createSyncTerrainNodesTask(getTerrainNodes, setTerrainNodes, getReady, setReady) {
|
|
32
|
+
return task((get, work) => {
|
|
33
|
+
const positionNode = get(terrainTasks.positionNode);
|
|
34
|
+
return work(() => {
|
|
35
|
+
const nextReady = positionNode != null;
|
|
36
|
+
const terrainNodes = getTerrainNodes();
|
|
37
|
+
if (terrainNodes.positionNode !== positionNode) {
|
|
38
|
+
setTerrainNodes({
|
|
39
|
+
positionNode
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (getReady() !== nextReady) {
|
|
43
|
+
setReady(nextReady);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
positionNode,
|
|
47
|
+
ready: nextReady
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}).displayName("syncTerrainNodesTask").cache("none");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resetOrSet(graph, ownedParamIds, ref, value, setValue) {
|
|
54
|
+
if (value === void 0) {
|
|
55
|
+
if (ownedParamIds.has(ref.id)) {
|
|
56
|
+
graph.reset(ref);
|
|
57
|
+
ownedParamIds.delete(ref.id);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
graph.set(ref, setValue);
|
|
62
|
+
ownedParamIds.add(ref.id);
|
|
63
|
+
}
|
|
64
|
+
function useTerrainParams(graph, options) {
|
|
65
|
+
const ownedParamIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
66
|
+
const {
|
|
67
|
+
rootSize: nextRootSize,
|
|
68
|
+
origin: nextOrigin,
|
|
69
|
+
maxLevel: nextMaxLevel,
|
|
70
|
+
maxNodes: nextMaxNodes,
|
|
71
|
+
innerTileSegments: nextInnerTileSegments,
|
|
72
|
+
skirtScale: nextSkirtScale,
|
|
73
|
+
elevationScale: nextElevationScale,
|
|
74
|
+
elevation,
|
|
75
|
+
surface: nextSurface,
|
|
76
|
+
terrainFieldFilter: nextTerrainFieldFilter
|
|
77
|
+
} = options;
|
|
78
|
+
useLayoutEffect(() => {
|
|
79
|
+
ownedParamIdsRef.current.clear();
|
|
80
|
+
}, [graph]);
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
const ownedParamIds = ownedParamIdsRef.current;
|
|
83
|
+
resetOrSet(
|
|
84
|
+
graph,
|
|
85
|
+
ownedParamIds,
|
|
86
|
+
rootSize,
|
|
87
|
+
nextRootSize,
|
|
88
|
+
() => nextRootSize
|
|
89
|
+
);
|
|
90
|
+
resetOrSet(graph, ownedParamIds, origin, nextOrigin, () => ({
|
|
91
|
+
x: nextOrigin?.x ?? 0,
|
|
92
|
+
y: nextOrigin?.y ?? 0,
|
|
93
|
+
z: nextOrigin?.z ?? 0
|
|
94
|
+
}));
|
|
95
|
+
resetOrSet(
|
|
96
|
+
graph,
|
|
97
|
+
ownedParamIds,
|
|
98
|
+
maxLevel,
|
|
99
|
+
nextMaxLevel,
|
|
100
|
+
() => nextMaxLevel
|
|
101
|
+
);
|
|
102
|
+
resetOrSet(
|
|
103
|
+
graph,
|
|
104
|
+
ownedParamIds,
|
|
105
|
+
maxNodes,
|
|
106
|
+
nextMaxNodes,
|
|
107
|
+
() => nextMaxNodes
|
|
108
|
+
);
|
|
109
|
+
resetOrSet(
|
|
110
|
+
graph,
|
|
111
|
+
ownedParamIds,
|
|
112
|
+
innerTileSegments,
|
|
113
|
+
nextInnerTileSegments,
|
|
114
|
+
() => nextInnerTileSegments
|
|
115
|
+
);
|
|
116
|
+
resetOrSet(
|
|
117
|
+
graph,
|
|
118
|
+
ownedParamIds,
|
|
119
|
+
skirtScale,
|
|
120
|
+
nextSkirtScale,
|
|
121
|
+
() => nextSkirtScale
|
|
122
|
+
);
|
|
123
|
+
resetOrSet(
|
|
124
|
+
graph,
|
|
125
|
+
ownedParamIds,
|
|
126
|
+
elevationScale,
|
|
127
|
+
nextElevationScale,
|
|
128
|
+
() => nextElevationScale
|
|
129
|
+
);
|
|
130
|
+
resetOrSet(
|
|
131
|
+
graph,
|
|
132
|
+
ownedParamIds,
|
|
133
|
+
surface,
|
|
134
|
+
nextSurface,
|
|
135
|
+
() => nextSurface
|
|
136
|
+
);
|
|
137
|
+
resetOrSet(
|
|
138
|
+
graph,
|
|
139
|
+
ownedParamIds,
|
|
140
|
+
terrainFieldFilter,
|
|
141
|
+
nextTerrainFieldFilter,
|
|
142
|
+
() => nextTerrainFieldFilter
|
|
143
|
+
);
|
|
144
|
+
if (elevation === void 0) {
|
|
145
|
+
if (ownedParamIds.has(elevationFn.id)) {
|
|
146
|
+
graph.reset(elevationFn);
|
|
147
|
+
ownedParamIds.delete(elevationFn.id);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
graph.set(elevationFn, () => elevation);
|
|
151
|
+
ownedParamIds.add(elevationFn.id);
|
|
152
|
+
}
|
|
153
|
+
}, [
|
|
154
|
+
graph,
|
|
155
|
+
nextRootSize,
|
|
156
|
+
nextOrigin,
|
|
157
|
+
nextMaxLevel,
|
|
158
|
+
nextMaxNodes,
|
|
159
|
+
nextInnerTileSegments,
|
|
160
|
+
nextSkirtScale,
|
|
161
|
+
nextElevationScale,
|
|
162
|
+
elevation,
|
|
163
|
+
nextSurface,
|
|
164
|
+
nextTerrainFieldFilter
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const WEBGPU_RENDERER_ERROR = "@hello-terrain/react requires a WebGPURenderer on <Canvas gl={...}>.";
|
|
169
|
+
const GRAPH_RUN_ERROR = "@hello-terrain/react terrain graph run failed.";
|
|
170
|
+
function toVector3Like(state, getCameraOrigin) {
|
|
171
|
+
return getCameraOrigin?.(state) ?? state.camera.position;
|
|
172
|
+
}
|
|
173
|
+
function getTerrainRunnerErrorKey(error) {
|
|
174
|
+
if (error instanceof Error) {
|
|
175
|
+
return `${error.name}:${error.message}`;
|
|
176
|
+
}
|
|
177
|
+
return String(error);
|
|
178
|
+
}
|
|
179
|
+
function isWebGpuRenderer(renderer) {
|
|
180
|
+
return typeof renderer === "object" && renderer !== null && "backend" in renderer;
|
|
181
|
+
}
|
|
182
|
+
function useTerrainRunner({
|
|
183
|
+
graph,
|
|
184
|
+
targets,
|
|
185
|
+
getCameraOrigin,
|
|
186
|
+
cameraHysteresis = 0.05
|
|
187
|
+
}) {
|
|
188
|
+
const graphRef = useRef(graph);
|
|
189
|
+
const targetsRef = useRef(targets);
|
|
190
|
+
const getCameraOriginRef = useRef(getCameraOrigin);
|
|
191
|
+
const lastCameraOriginRef = useRef(null);
|
|
192
|
+
const runningRef = useRef(false);
|
|
193
|
+
const generationRef = useRef(0);
|
|
194
|
+
const runAbortControllerRef = useRef(null);
|
|
195
|
+
const runPromiseRef = useRef(null);
|
|
196
|
+
const lastErrorKeyRef = useRef(null);
|
|
197
|
+
const reportError = useCallback(
|
|
198
|
+
(error, errorKey) => {
|
|
199
|
+
const nextErrorKey = errorKey ?? getTerrainRunnerErrorKey(error);
|
|
200
|
+
if (lastErrorKeyRef.current === nextErrorKey) return;
|
|
201
|
+
lastErrorKeyRef.current = nextErrorKey;
|
|
202
|
+
console.error(error);
|
|
203
|
+
},
|
|
204
|
+
[]
|
|
205
|
+
);
|
|
206
|
+
const clearError = useCallback(() => {
|
|
207
|
+
lastErrorKeyRef.current = null;
|
|
208
|
+
}, []);
|
|
209
|
+
const stopCurrentRun = useCallback(async () => {
|
|
210
|
+
const activeController = runAbortControllerRef.current;
|
|
211
|
+
if (activeController && !activeController.signal.aborted) {
|
|
212
|
+
activeController.abort(new Error("Terrain runner stopped"));
|
|
213
|
+
}
|
|
214
|
+
const activeRun = runPromiseRef.current;
|
|
215
|
+
if (activeRun) {
|
|
216
|
+
await activeRun.catch(() => {
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}, []);
|
|
220
|
+
const updateCameraOrigin = useCallback(
|
|
221
|
+
(state, activeGraph) => {
|
|
222
|
+
const activeGetCameraOrigin = getCameraOriginRef.current;
|
|
223
|
+
const cameraOrigin = toVector3Like(state, activeGetCameraOrigin);
|
|
224
|
+
const nextOrigin = new Vector3(
|
|
225
|
+
cameraOrigin.x,
|
|
226
|
+
cameraOrigin.y,
|
|
227
|
+
cameraOrigin.z
|
|
228
|
+
);
|
|
229
|
+
const lastOrigin = lastCameraOriginRef.current;
|
|
230
|
+
const hysteresisSq = cameraHysteresis * cameraHysteresis;
|
|
231
|
+
if (!lastOrigin || lastOrigin.distanceToSquared(nextOrigin) >= hysteresisSq) {
|
|
232
|
+
activeGraph.set(quadtreeUpdate, (prev) => {
|
|
233
|
+
prev.cameraOrigin.x = nextOrigin.x;
|
|
234
|
+
prev.cameraOrigin.y = nextOrigin.y;
|
|
235
|
+
prev.cameraOrigin.z = nextOrigin.z;
|
|
236
|
+
return prev;
|
|
237
|
+
});
|
|
238
|
+
lastCameraOriginRef.current = nextOrigin;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
[cameraHysteresis]
|
|
242
|
+
);
|
|
243
|
+
const finishRun = useCallback(
|
|
244
|
+
(activeRunController, activeGeneration) => {
|
|
245
|
+
if (runAbortControllerRef.current === activeRunController) {
|
|
246
|
+
runAbortControllerRef.current = null;
|
|
247
|
+
runPromiseRef.current = null;
|
|
248
|
+
}
|
|
249
|
+
if (generationRef.current === activeGeneration) {
|
|
250
|
+
runningRef.current = false;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
[]
|
|
254
|
+
);
|
|
255
|
+
useLayoutEffect(() => {
|
|
256
|
+
void stopCurrentRun();
|
|
257
|
+
graphRef.current = graph;
|
|
258
|
+
targetsRef.current = targets;
|
|
259
|
+
getCameraOriginRef.current = getCameraOrigin;
|
|
260
|
+
lastCameraOriginRef.current = null;
|
|
261
|
+
runningRef.current = false;
|
|
262
|
+
generationRef.current += 1;
|
|
263
|
+
clearError();
|
|
264
|
+
return () => {
|
|
265
|
+
generationRef.current += 1;
|
|
266
|
+
runningRef.current = false;
|
|
267
|
+
void stopCurrentRun();
|
|
268
|
+
};
|
|
269
|
+
}, [clearError, getCameraOrigin, graph, stopCurrentRun, targets]);
|
|
270
|
+
useFrame((state) => {
|
|
271
|
+
if (runningRef.current) return;
|
|
272
|
+
const renderer = state.gl;
|
|
273
|
+
if (!isWebGpuRenderer(renderer)) {
|
|
274
|
+
reportError(
|
|
275
|
+
new Error(WEBGPU_RENDERER_ERROR),
|
|
276
|
+
`renderer:${WEBGPU_RENDERER_ERROR}`
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const activeGeneration = generationRef.current;
|
|
281
|
+
const activeRunController = new AbortController();
|
|
282
|
+
runningRef.current = true;
|
|
283
|
+
runAbortControllerRef.current = activeRunController;
|
|
284
|
+
const runPromise = (async () => {
|
|
285
|
+
try {
|
|
286
|
+
const activeGraph = graphRef.current;
|
|
287
|
+
const activeTargets = targetsRef.current;
|
|
288
|
+
updateCameraOrigin(state, activeGraph);
|
|
289
|
+
const report = await activeGraph.run({
|
|
290
|
+
targets: activeTargets,
|
|
291
|
+
signal: activeRunController.signal,
|
|
292
|
+
resources: {
|
|
293
|
+
renderer
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
if (report.status === "error") {
|
|
297
|
+
reportError(new Error(GRAPH_RUN_ERROR), `graph:${GRAPH_RUN_ERROR}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
clearError();
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (activeRunController.signal.aborted) return;
|
|
303
|
+
reportError(error);
|
|
304
|
+
} finally {
|
|
305
|
+
finishRun(activeRunController, activeGeneration);
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
runPromiseRef.current = runPromise;
|
|
309
|
+
});
|
|
310
|
+
return stopCurrentRun;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function useTerrain(options = {}) {
|
|
314
|
+
const runtime = useMemo(
|
|
315
|
+
() => ({
|
|
316
|
+
query: null,
|
|
317
|
+
raycast: null
|
|
318
|
+
}),
|
|
319
|
+
[]
|
|
320
|
+
);
|
|
321
|
+
const isMountedRef = useRef(true);
|
|
322
|
+
const graphLifecycleRef = useRef(0);
|
|
323
|
+
const terrainNodesRef = useRef({
|
|
324
|
+
positionNode: null
|
|
325
|
+
});
|
|
326
|
+
const [terrainNodes, setTerrainNodes] = useState(terrainNodesRef.current);
|
|
327
|
+
const readyRef = useRef(false);
|
|
328
|
+
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
|
+
);
|
|
347
|
+
const emitReady = useCallback((nextReady) => {
|
|
348
|
+
if (!isMountedRef.current) return;
|
|
349
|
+
readyRef.current = nextReady;
|
|
350
|
+
setReady((prevReady) => prevReady === nextReady ? prevReady : nextReady);
|
|
351
|
+
}, []);
|
|
352
|
+
const getReady = useCallback(
|
|
353
|
+
() => readyRef.current,
|
|
354
|
+
[]
|
|
355
|
+
);
|
|
356
|
+
const syncTerrainRuntimeTask = useMemo(
|
|
357
|
+
() => createSyncTerrainRuntimeTask(runtime),
|
|
358
|
+
[runtime]
|
|
359
|
+
);
|
|
360
|
+
const syncTerrainNodesTask = useMemo(
|
|
361
|
+
() => createSyncTerrainNodesTask(
|
|
362
|
+
getTerrainNodes,
|
|
363
|
+
emitTerrainNodes,
|
|
364
|
+
getReady,
|
|
365
|
+
emitReady
|
|
366
|
+
),
|
|
367
|
+
[emitReady, emitTerrainNodes, getReady, getTerrainNodes]
|
|
368
|
+
);
|
|
369
|
+
const graph = useMemo(() => {
|
|
370
|
+
const nextGraph = terrainGraph();
|
|
371
|
+
for (const task of options.tasks ?? []) {
|
|
372
|
+
nextGraph.add(task);
|
|
373
|
+
}
|
|
374
|
+
nextGraph.add(syncTerrainRuntimeTask);
|
|
375
|
+
nextGraph.add(syncTerrainNodesTask);
|
|
376
|
+
return nextGraph;
|
|
377
|
+
}, [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
|
+
);
|
|
391
|
+
const stopTerrainRunner = useTerrainRunner({
|
|
392
|
+
graph,
|
|
393
|
+
targets: runnerTargets,
|
|
394
|
+
getCameraOrigin: options.getCameraOrigin,
|
|
395
|
+
cameraHysteresis: options.cameraHysteresis
|
|
396
|
+
});
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
graphLifecycleRef.current += 1;
|
|
399
|
+
const lifecycle = graphLifecycleRef.current;
|
|
400
|
+
isMountedRef.current = true;
|
|
401
|
+
return () => {
|
|
402
|
+
isMountedRef.current = false;
|
|
403
|
+
queueMicrotask(() => {
|
|
404
|
+
if (graphLifecycleRef.current !== lifecycle) return;
|
|
405
|
+
void stopTerrainRunner().finally(() => {
|
|
406
|
+
if (graphLifecycleRef.current !== lifecycle) return;
|
|
407
|
+
graph.dispose();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
}, [graph, stopTerrainRunner]);
|
|
412
|
+
useLayoutEffect(() => {
|
|
413
|
+
runtime.query = null;
|
|
414
|
+
runtime.raycast = null;
|
|
415
|
+
const nextTerrainNodes = {
|
|
416
|
+
positionNode: null
|
|
417
|
+
};
|
|
418
|
+
terrainNodesRef.current = nextTerrainNodes;
|
|
419
|
+
readyRef.current = false;
|
|
420
|
+
setTerrainNodes(nextTerrainNodes);
|
|
421
|
+
setReady(false);
|
|
422
|
+
}, [graph, runtime]);
|
|
423
|
+
useTerrainParams(graph, options);
|
|
424
|
+
return useMemo(
|
|
425
|
+
() => ({
|
|
426
|
+
graph,
|
|
427
|
+
tasks: terrainTasks,
|
|
428
|
+
runtime,
|
|
429
|
+
ready,
|
|
430
|
+
...terrainNodes
|
|
431
|
+
}),
|
|
432
|
+
[graph, ready, runtime, terrainNodes]
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function useTerrainMesh(innerTileSegments, maxNodes) {
|
|
437
|
+
const [mesh] = useState(
|
|
438
|
+
() => new TerrainMesh({
|
|
439
|
+
innerTileSegments: innerTileSegments ?? 13,
|
|
440
|
+
maxNodes: maxNodes ?? 1024
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
mesh.innerTileSegments = innerTileSegments ?? 13;
|
|
445
|
+
}, [mesh, innerTileSegments]);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
mesh.maxNodes = maxNodes ?? 1024;
|
|
448
|
+
}, [mesh, maxNodes]);
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
return () => {
|
|
451
|
+
mesh.geometry.dispose();
|
|
452
|
+
};
|
|
453
|
+
}, [mesh]);
|
|
454
|
+
return mesh;
|
|
455
|
+
}
|
|
456
|
+
function syncTerrainMesh(mesh, terrain) {
|
|
457
|
+
const leaves = terrain.graph.peek(terrainTasks.quadtreeUpdate);
|
|
458
|
+
if (leaves && mesh.count !== leaves.count) {
|
|
459
|
+
mesh.count = leaves.count;
|
|
460
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
461
|
+
}
|
|
462
|
+
const raycast = terrain.runtime.raycast;
|
|
463
|
+
if (mesh.terrainRaycast !== raycast) {
|
|
464
|
+
mesh.terrainRaycast = raycast;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function attachTerrainMaterial(node, terrainNodes) {
|
|
468
|
+
if (!isValidElement(node)) return node;
|
|
469
|
+
const nextKey = `terrain-material-${terrainNodes.positionNode?.id ?? "null"}`;
|
|
470
|
+
if ("attach" in node.props && node.props.attach != null) {
|
|
471
|
+
return cloneElement(node, { key: nextKey });
|
|
472
|
+
}
|
|
473
|
+
return cloneElement(node, { attach: "material", key: nextKey });
|
|
474
|
+
}
|
|
475
|
+
function TerrainWithHandle({
|
|
476
|
+
terrain,
|
|
477
|
+
children,
|
|
478
|
+
innerTileSegments,
|
|
479
|
+
maxNodes,
|
|
480
|
+
...primitiveProps
|
|
481
|
+
}) {
|
|
482
|
+
const mesh = useTerrainMesh(innerTileSegments, maxNodes);
|
|
483
|
+
const { visible: primitiveVisible = true, ...restPrimitiveProps } = primitiveProps;
|
|
484
|
+
useFrame(() => {
|
|
485
|
+
syncTerrainMesh(mesh, terrain);
|
|
486
|
+
});
|
|
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
|
+
) });
|
|
501
|
+
}
|
|
502
|
+
function InternalTerrain(props) {
|
|
503
|
+
const {
|
|
504
|
+
children,
|
|
505
|
+
rootSize,
|
|
506
|
+
origin,
|
|
507
|
+
maxLevel,
|
|
508
|
+
innerTileSegments,
|
|
509
|
+
skirtScale,
|
|
510
|
+
elevationScale,
|
|
511
|
+
elevation,
|
|
512
|
+
surface,
|
|
513
|
+
terrainFieldFilter,
|
|
514
|
+
getCameraOrigin,
|
|
515
|
+
cameraHysteresis,
|
|
516
|
+
tasks,
|
|
517
|
+
maxNodes,
|
|
518
|
+
...primitiveProps
|
|
519
|
+
} = props;
|
|
520
|
+
const terrain = useTerrain({
|
|
521
|
+
rootSize,
|
|
522
|
+
origin,
|
|
523
|
+
maxLevel,
|
|
524
|
+
innerTileSegments,
|
|
525
|
+
skirtScale,
|
|
526
|
+
elevationScale,
|
|
527
|
+
elevation,
|
|
528
|
+
surface,
|
|
529
|
+
terrainFieldFilter,
|
|
530
|
+
getCameraOrigin,
|
|
531
|
+
cameraHysteresis,
|
|
532
|
+
tasks,
|
|
533
|
+
maxNodes
|
|
534
|
+
});
|
|
535
|
+
return /* @__PURE__ */ jsx(
|
|
536
|
+
TerrainWithHandle,
|
|
537
|
+
{
|
|
538
|
+
terrain,
|
|
539
|
+
innerTileSegments,
|
|
540
|
+
maxNodes,
|
|
541
|
+
...primitiveProps,
|
|
542
|
+
children
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
function Terrain({
|
|
547
|
+
terrain: providedTerrain,
|
|
548
|
+
children,
|
|
549
|
+
rootSize,
|
|
550
|
+
origin,
|
|
551
|
+
maxLevel,
|
|
552
|
+
innerTileSegments,
|
|
553
|
+
skirtScale,
|
|
554
|
+
elevationScale,
|
|
555
|
+
elevation,
|
|
556
|
+
surface,
|
|
557
|
+
terrainFieldFilter,
|
|
558
|
+
getCameraOrigin,
|
|
559
|
+
cameraHysteresis,
|
|
560
|
+
tasks,
|
|
561
|
+
maxNodes,
|
|
562
|
+
...primitiveProps
|
|
563
|
+
}) {
|
|
564
|
+
if (providedTerrain) {
|
|
565
|
+
return /* @__PURE__ */ jsx(
|
|
566
|
+
TerrainWithHandle,
|
|
567
|
+
{
|
|
568
|
+
terrain: providedTerrain,
|
|
569
|
+
innerTileSegments,
|
|
570
|
+
maxNodes,
|
|
571
|
+
...primitiveProps,
|
|
572
|
+
children
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return /* @__PURE__ */ jsx(
|
|
577
|
+
InternalTerrain,
|
|
578
|
+
{
|
|
579
|
+
rootSize,
|
|
580
|
+
origin,
|
|
581
|
+
maxLevel,
|
|
582
|
+
innerTileSegments,
|
|
583
|
+
skirtScale,
|
|
584
|
+
elevationScale,
|
|
585
|
+
elevation,
|
|
586
|
+
surface,
|
|
587
|
+
terrainFieldFilter,
|
|
588
|
+
getCameraOrigin,
|
|
589
|
+
cameraHysteresis,
|
|
590
|
+
tasks,
|
|
591
|
+
maxNodes,
|
|
592
|
+
...primitiveProps,
|
|
593
|
+
children
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export { Terrain, TerrainProvider, useTerrain, useTerrainContext };
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hello-terrain/react",
|
|
3
|
+
"description": "High performance terrain system for three.js and react-three/fiber",
|
|
4
|
+
"homepage": "http://hello-terrain.kenny.wtf",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/kenjinp/hello-terrain.git"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.0.0-alpha.10",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.mjs",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hello-terrain/three": "0.0.0-alpha.10",
|
|
26
|
+
"@hello-terrain/work": "0.3.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@testing-library/dom": "^10.4.0",
|
|
30
|
+
"@testing-library/react": "^16.2.0",
|
|
31
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
32
|
+
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
|
33
|
+
"@types/three": ">=0.182.0",
|
|
34
|
+
"jsdom": "^26.0.0",
|
|
35
|
+
"unbuild": "^3.5.0",
|
|
36
|
+
"vitest": "^4.0.17",
|
|
37
|
+
"@config/oxfmt": "0.1.0",
|
|
38
|
+
"@config/typescript": "0.1.0",
|
|
39
|
+
"@config/oxlint": "0.1.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@react-three/fiber": "^8.0.0 || ^9.0.0",
|
|
43
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
|
+
"three": ">=0.182.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "unbuild",
|
|
52
|
+
"release": "pnpm run build && pnpm publish --access=public",
|
|
53
|
+
"test": "vitest",
|
|
54
|
+
"lint": "oxlint -c node_modules/@config/oxlint/react.json",
|
|
55
|
+
"format": "oxfmt -c node_modules/@config/oxfmt/base.json --write ."
|
|
56
|
+
}
|
|
57
|
+
}
|