@jgengine/shell 0.10.0 → 0.11.0
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/CHANGELOG.md +13 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayerShell.d.ts +0 -12
- package/dist/GamePlayerShell.js +88 -1043
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.js +30 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +2 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +94 -24
- package/dist/camera/rigMath.js +100 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/cartridge.d.ts +1 -1
- package/dist/cartridge.js +3 -3
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +25 -6
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
- package/dist/devtools/DevtoolsOverlay.js +19 -450
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +262 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +32 -6
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.js +47 -9
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/registry.d.ts +3 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +73 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +39 -3
- package/dist/render/resolveModel.js +79 -2
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +130 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +94 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +82 -0
- package/dist/scene/AuthoredScene.js +173 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +25 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +21 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +37 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +22 -0
- package/dist/scene/sceneKindRenderers.d.ts +28 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +5 -0
- package/dist/settings/appliedSettings.js +9 -0
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +43 -0
- package/dist/structures/TransformGizmo.js +55 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.js +1 -1
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +3 -0
- package/dist/terrain/grassMaterial.d.ts +2 -0
- package/dist/terrain/grassMaterial.js +2 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +10 -0
- package/dist/terrain/soilPatchMaterial.js +109 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +72 -5
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/OceanConfig.d.ts +2 -0
- package/dist/water/OceanConfig.js +2 -0
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +159 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +38 -0
- package/skills/jgengine-world/api.md +2652 -0
- package/skills/jgengine-world/capabilities.md +370 -0
- package/skills/jgengine-world/reference.md +222 -0
- package/llms.txt +0 -2761
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useLoader } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
|
+
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
7
|
+
import { chunkScatterInstances } from "@jgengine/core/world/scatterRegion";
|
|
8
|
+
import { buildScatterProxy } from "./scatterProxies.js";
|
|
9
|
+
import { buildScatterModelSources, disposeScatterModelSources } from "./scatterModels.js";
|
|
10
|
+
/** Chunk edge (m): dense fields split into this-sized buckets, each frustum-culled independently. */
|
|
11
|
+
const DEFAULT_CHUNK_SIZE = 24;
|
|
12
|
+
const DEFAULT_MAX_INSTANCES = 20000;
|
|
13
|
+
/** Groups resolved instances by spatial chunk then species so each batch is one cullable draw. */
|
|
14
|
+
function batchInstances(instances, chunkSize) {
|
|
15
|
+
const batches = [];
|
|
16
|
+
const matrix = new THREE.Matrix4();
|
|
17
|
+
const position = new THREE.Vector3();
|
|
18
|
+
const quaternion = new THREE.Quaternion();
|
|
19
|
+
const euler = new THREE.Euler();
|
|
20
|
+
const scale = new THREE.Vector3();
|
|
21
|
+
for (const chunk of chunkScatterInstances(instances, chunkSize)) {
|
|
22
|
+
const byItem = new Map();
|
|
23
|
+
for (const instance of chunk.instances) {
|
|
24
|
+
position.set(instance.x, instance.y, instance.z);
|
|
25
|
+
euler.set(0, instance.rotationY, 0);
|
|
26
|
+
quaternion.setFromEuler(euler);
|
|
27
|
+
scale.set(instance.scale, instance.scale, instance.scale);
|
|
28
|
+
matrix.compose(position, quaternion, scale);
|
|
29
|
+
const bucket = byItem.get(instance.item);
|
|
30
|
+
if (bucket === undefined)
|
|
31
|
+
byItem.set(instance.item, [matrix.clone()]);
|
|
32
|
+
else
|
|
33
|
+
bucket.push(matrix.clone());
|
|
34
|
+
}
|
|
35
|
+
for (const [item, matrices] of byItem)
|
|
36
|
+
batches.push({ key: `${chunk.key}:${item}`, item, matrices });
|
|
37
|
+
}
|
|
38
|
+
return batches;
|
|
39
|
+
}
|
|
40
|
+
/** One species-in-a-chunk InstancedMesh; frustum-culled as a unit so offscreen chunks cost nothing. */
|
|
41
|
+
function ScatterBatchMesh({ batch, geometry }) {
|
|
42
|
+
const meshRef = useRef(null);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const mesh = meshRef.current;
|
|
45
|
+
if (mesh === null)
|
|
46
|
+
return;
|
|
47
|
+
for (let i = 0; i < batch.matrices.length; i += 1)
|
|
48
|
+
mesh.setMatrixAt(i, batch.matrices[i]);
|
|
49
|
+
mesh.count = batch.matrices.length;
|
|
50
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
51
|
+
mesh.computeBoundingSphere();
|
|
52
|
+
}, [batch]);
|
|
53
|
+
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, undefined, batch.matrices.length], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 0.85, metalness: 0 }) }));
|
|
54
|
+
}
|
|
55
|
+
/** One GLB-backed draw source instanced across every chunk of one species. */
|
|
56
|
+
function ScatterModelSourceInstances({ source, matrices }) {
|
|
57
|
+
const meshRef = useRef(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const mesh = meshRef.current;
|
|
60
|
+
if (mesh === null)
|
|
61
|
+
return;
|
|
62
|
+
const composed = new THREE.Matrix4();
|
|
63
|
+
for (let i = 0; i < matrices.length; i += 1) {
|
|
64
|
+
composed.multiplyMatrices(matrices[i], source.localMatrix);
|
|
65
|
+
mesh.setMatrixAt(i, composed);
|
|
66
|
+
}
|
|
67
|
+
mesh.count = matrices.length;
|
|
68
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
69
|
+
mesh.computeBoundingSphere();
|
|
70
|
+
}, [matrices, source]);
|
|
71
|
+
return _jsx("instancedMesh", { ref: meshRef, args: [source.geometry, source.material, matrices.length], castShadow: true, receiveShadow: true });
|
|
72
|
+
}
|
|
73
|
+
/** Loads one species' real catalog GLB once, harvests its instanceable draw sources, then GPU-instances them across every chunk that species appears in. */
|
|
74
|
+
function ScatterModelSpecies({ model, batches }) {
|
|
75
|
+
const gltf = useLoader(GLTFLoader, model.url, (loader) => {
|
|
76
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
77
|
+
});
|
|
78
|
+
const { sources, root } = useMemo(() => buildScatterModelSources(gltf.scene, model), [gltf, model]);
|
|
79
|
+
useEffect(() => () => disposeScatterModelSources(root), [root]);
|
|
80
|
+
return (_jsx(_Fragment, { children: sources.map((source, sourceIndex) => batches.map((batch) => (_jsx(ScatterModelSourceInstances, { source: source, matrices: batch.matrices }, `${batch.key}:${sourceIndex}`)))) }));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Renders resolved {@link ScatterInstance}s grouped into per-chunk, per-species GPU-instanced draws
|
|
84
|
+
* that frustum-cull independently. Species with no `resolveItem` mapping (or a `null` result) render
|
|
85
|
+
* as stylized proxy models (trunked trees, stacked pines, round bushes, faceted rocks, grass tufts);
|
|
86
|
+
* species `resolveItem` resolves to a `ModelConfig` GPU-instance the real catalog GLB instead. The one
|
|
87
|
+
* runtime foliage renderer shared by the editor's scatter preview and games that consume
|
|
88
|
+
* `resolveScatter` — never one node per placement.
|
|
89
|
+
*/
|
|
90
|
+
export function InstancedScatter({ instances, chunkSize = DEFAULT_CHUNK_SIZE, maxInstances = DEFAULT_MAX_INSTANCES, resolveItem, }) {
|
|
91
|
+
const batches = useMemo(() => batchInstances(instances.length > maxInstances ? instances.slice(0, maxInstances) : instances, chunkSize), [instances, chunkSize, maxInstances]);
|
|
92
|
+
const { proxyBatches, modelSpecies } = useMemo(() => {
|
|
93
|
+
const proxy = [];
|
|
94
|
+
const bySpecies = new Map();
|
|
95
|
+
const knownProxy = new Set();
|
|
96
|
+
for (const batch of batches) {
|
|
97
|
+
const species = bySpecies.get(batch.item);
|
|
98
|
+
if (species !== undefined) {
|
|
99
|
+
species.batches.push(batch);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (knownProxy.has(batch.item)) {
|
|
103
|
+
proxy.push(batch);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const resolved = resolveItem?.(batch.item) ?? null;
|
|
107
|
+
if (resolved === null) {
|
|
108
|
+
knownProxy.add(batch.item);
|
|
109
|
+
proxy.push(batch);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
bySpecies.set(batch.item, { model: resolved, batches: [batch] });
|
|
113
|
+
}
|
|
114
|
+
return { proxyBatches: proxy, modelSpecies: [...bySpecies.entries()] };
|
|
115
|
+
}, [batches, resolveItem]);
|
|
116
|
+
const geometries = useMemo(() => {
|
|
117
|
+
const items = new Set(proxyBatches.map((batch) => batch.item));
|
|
118
|
+
const map = new Map();
|
|
119
|
+
for (const item of items)
|
|
120
|
+
map.set(item, buildScatterProxy(item));
|
|
121
|
+
return map;
|
|
122
|
+
}, [proxyBatches]);
|
|
123
|
+
useEffect(() => () => geometries.forEach((geometry) => geometry.dispose()), [geometries]);
|
|
124
|
+
if (batches.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
return (_jsxs(_Fragment, { children: [proxyBatches.map((batch) => {
|
|
127
|
+
const geometry = geometries.get(batch.item);
|
|
128
|
+
return geometry === undefined ? null : _jsx(ScatterBatchMesh, { batch: batch, geometry: geometry }, batch.key);
|
|
129
|
+
}), modelSpecies.map(([item, species]) => (_jsx(Suspense, { fallback: null, children: _jsx(ScatterModelSpecies, { model: species.model, batches: species.batches }) }, item)))] }));
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/** One instanceable draw source harvested from a resolved GLB — geometry stays shared with the loader cache, `localMatrix` bakes the model's own transform (from its scene root) plus scale/anchor normalization. */
|
|
4
|
+
export interface ScatterModelSource {
|
|
5
|
+
geometry: THREE.BufferGeometry;
|
|
6
|
+
material: THREE.Material | THREE.Material[];
|
|
7
|
+
localMatrix: THREE.Matrix4;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Clones a loaded GLTF scene (so shared cache materials stay untouched), applies the model's material
|
|
11
|
+
* override, then harvests every mesh as a {@link ScatterModelSource} — one `InstancedMesh` draw per
|
|
12
|
+
* source, matrices composed per-instance at render time. Geometry is left unowned (still the loader
|
|
13
|
+
* cache's), only the cloned materials need disposal via {@link disposeScatterModelSources}.
|
|
14
|
+
* @internal — the GLB-harvesting behind `InstancedScatter`'s `resolveItem` override.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildScatterModelSources(gltfScene: THREE.Object3D, model: ModelConfig): {
|
|
17
|
+
sources: ScatterModelSource[];
|
|
18
|
+
root: THREE.Object3D;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Disposes the cloned materials harvested by {@link buildScatterModelSources}; never disposes geometry
|
|
22
|
+
* (still owned by the loader cache).
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function disposeScatterModelSources(root: THREE.Object3D): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { applyMaterialOverride } from "../materialOverride.js";
|
|
3
|
+
import { cloneModelScene, disposeClonedMaterials } from "../render/modelRender.js";
|
|
4
|
+
/** The scale + position offset a `ModelConfig` applies before any per-instance transform — same anchor/targetHeight rules as the single-instance entity renderer. */
|
|
5
|
+
function scatterModelBaseTransform(scene, model) {
|
|
6
|
+
let scale = model.scale ?? 1;
|
|
7
|
+
let minY = 0;
|
|
8
|
+
let centerX = 0;
|
|
9
|
+
let centerZ = 0;
|
|
10
|
+
if (model.targetHeight !== undefined) {
|
|
11
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
12
|
+
const height = box.max.y - box.min.y;
|
|
13
|
+
if (Number.isFinite(height) && height > 0) {
|
|
14
|
+
scale *= model.targetHeight / height;
|
|
15
|
+
minY = box.min.y;
|
|
16
|
+
centerX = (box.min.x + box.max.x) / 2;
|
|
17
|
+
centerZ = (box.min.z + box.max.z) / 2;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if ((model.anchor ?? "center") === "center" && model.dims !== undefined) {
|
|
21
|
+
minY = model.dims.minY;
|
|
22
|
+
centerX = model.dims.center.x;
|
|
23
|
+
centerZ = model.dims.center.z;
|
|
24
|
+
}
|
|
25
|
+
const baseY = model.y ?? 0;
|
|
26
|
+
return new THREE.Matrix4().compose(new THREE.Vector3(-scale * centerX, baseY - scale * minY, -scale * centerZ), new THREE.Quaternion(), new THREE.Vector3(scale, scale, scale));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Clones a loaded GLTF scene (so shared cache materials stay untouched), applies the model's material
|
|
30
|
+
* override, then harvests every mesh as a {@link ScatterModelSource} — one `InstancedMesh` draw per
|
|
31
|
+
* source, matrices composed per-instance at render time. Geometry is left unowned (still the loader
|
|
32
|
+
* cache's), only the cloned materials need disposal via {@link disposeScatterModelSources}.
|
|
33
|
+
* @internal — the GLB-harvesting behind `InstancedScatter`'s `resolveItem` override.
|
|
34
|
+
*/
|
|
35
|
+
export function buildScatterModelSources(gltfScene, model) {
|
|
36
|
+
const root = cloneModelScene(gltfScene);
|
|
37
|
+
if (model.material !== undefined)
|
|
38
|
+
applyMaterialOverride(root, model.material, { clone: false });
|
|
39
|
+
root.updateMatrixWorld(true);
|
|
40
|
+
const base = scatterModelBaseTransform(root, model);
|
|
41
|
+
const sources = [];
|
|
42
|
+
root.traverse((node) => {
|
|
43
|
+
const mesh = node;
|
|
44
|
+
if (!mesh.isMesh)
|
|
45
|
+
return;
|
|
46
|
+
sources.push({
|
|
47
|
+
geometry: mesh.geometry,
|
|
48
|
+
material: mesh.material,
|
|
49
|
+
localMatrix: base.clone().multiply(mesh.matrixWorld),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return { sources, root };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Disposes the cloned materials harvested by {@link buildScatterModelSources}; never disposes geometry
|
|
56
|
+
* (still owned by the loader cache).
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export function disposeScatterModelSources(root) {
|
|
60
|
+
disposeClonedMaterials(root);
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the merged proxy geometry for a scatter item id, memoized per builder call by the caller.
|
|
4
|
+
* @internal — the geometry catalog behind the `InstancedScatter` renderer.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildScatterProxy(item: string): THREE.BufferGeometry;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const BARK = "#5b4127";
|
|
3
|
+
const LEAF = "#3f6b30";
|
|
4
|
+
const PINE = "#2f5330";
|
|
5
|
+
const OAK = "#4a7a37";
|
|
6
|
+
const BUSH = "#4d7d3a";
|
|
7
|
+
const ROCK = "#82858c";
|
|
8
|
+
const GRASS = "#5f9a3c";
|
|
9
|
+
/** Concatenates part geometries into one vertex-colored buffer (position + normal + color). */
|
|
10
|
+
function mergeParts(parts) {
|
|
11
|
+
const geos = parts.map((part) => (part.geometry.index === null ? part.geometry : part.geometry.toNonIndexed()));
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const geo of geos)
|
|
14
|
+
total += geo.attributes.position.count;
|
|
15
|
+
const position = new Float32Array(total * 3);
|
|
16
|
+
const normal = new Float32Array(total * 3);
|
|
17
|
+
const color = new Float32Array(total * 3);
|
|
18
|
+
const tint = new THREE.Color();
|
|
19
|
+
let offset = 0;
|
|
20
|
+
for (let i = 0; i < geos.length; i += 1) {
|
|
21
|
+
const geo = geos[i];
|
|
22
|
+
const pos = geo.attributes.position;
|
|
23
|
+
const nor = geo.attributes.normal;
|
|
24
|
+
position.set(pos.array, offset * 3);
|
|
25
|
+
normal.set(nor.array, offset * 3);
|
|
26
|
+
tint.set(parts[i].color);
|
|
27
|
+
for (let v = 0; v < pos.count; v += 1) {
|
|
28
|
+
color[(offset + v) * 3] = tint.r;
|
|
29
|
+
color[(offset + v) * 3 + 1] = tint.g;
|
|
30
|
+
color[(offset + v) * 3 + 2] = tint.b;
|
|
31
|
+
}
|
|
32
|
+
offset += pos.count;
|
|
33
|
+
}
|
|
34
|
+
for (const geo of geos)
|
|
35
|
+
geo.dispose();
|
|
36
|
+
const merged = new THREE.BufferGeometry();
|
|
37
|
+
merged.setAttribute("position", new THREE.BufferAttribute(position, 3));
|
|
38
|
+
merged.setAttribute("normal", new THREE.BufferAttribute(normal, 3));
|
|
39
|
+
merged.setAttribute("color", new THREE.BufferAttribute(color, 3));
|
|
40
|
+
merged.computeBoundingSphere();
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
function trunk(height, radius) {
|
|
44
|
+
const geo = new THREE.CylinderGeometry(radius * 0.8, radius, height, 5);
|
|
45
|
+
geo.translate(0, height / 2, 0);
|
|
46
|
+
return geo;
|
|
47
|
+
}
|
|
48
|
+
function cone(radius, height, y, segments = 7) {
|
|
49
|
+
const geo = new THREE.ConeGeometry(radius, height, segments);
|
|
50
|
+
geo.translate(0, y + height / 2, 0);
|
|
51
|
+
return geo;
|
|
52
|
+
}
|
|
53
|
+
function sphere(radius, y, squash = 1) {
|
|
54
|
+
const geo = new THREE.IcosahedronGeometry(radius, 1);
|
|
55
|
+
geo.scale(1, squash, 1);
|
|
56
|
+
geo.translate(0, y, 0);
|
|
57
|
+
return geo;
|
|
58
|
+
}
|
|
59
|
+
const BUILDERS = {
|
|
60
|
+
tree: () => mergeParts([
|
|
61
|
+
{ geometry: trunk(1.1, 0.12), color: BARK },
|
|
62
|
+
{ geometry: cone(0.8, 1.7, 0.9), color: LEAF },
|
|
63
|
+
]),
|
|
64
|
+
pine: () => mergeParts([
|
|
65
|
+
{ geometry: trunk(0.9, 0.1), color: BARK },
|
|
66
|
+
{ geometry: cone(0.7, 1.0, 0.7), color: PINE },
|
|
67
|
+
{ geometry: cone(0.55, 0.9, 1.35), color: PINE },
|
|
68
|
+
{ geometry: cone(0.38, 0.8, 1.95), color: PINE },
|
|
69
|
+
]),
|
|
70
|
+
oak: () => mergeParts([
|
|
71
|
+
{ geometry: trunk(1.0, 0.14), color: BARK },
|
|
72
|
+
{ geometry: sphere(0.85, 1.55, 0.85), color: OAK },
|
|
73
|
+
]),
|
|
74
|
+
bush: () => mergeParts([{ geometry: sphere(0.55, 0.45, 0.8), color: BUSH }]),
|
|
75
|
+
shrub: () => mergeParts([{ geometry: sphere(0.5, 0.4, 0.75), color: BUSH }]),
|
|
76
|
+
rock: () => mergeParts([{ geometry: sphere(0.5, 0.28, 0.7), color: ROCK }]),
|
|
77
|
+
stone: () => mergeParts([{ geometry: sphere(0.4, 0.22, 0.65), color: ROCK }]),
|
|
78
|
+
grass: () => mergeParts([
|
|
79
|
+
{ geometry: cone(0.09, 0.7, 0), color: GRASS },
|
|
80
|
+
{ geometry: cone(0.08, 0.55, 0).translate(0.12, 0, 0.05), color: GRASS },
|
|
81
|
+
{ geometry: cone(0.08, 0.6, 0).translate(-0.1, 0, -0.06), color: GRASS },
|
|
82
|
+
]),
|
|
83
|
+
};
|
|
84
|
+
function defaultProxy() {
|
|
85
|
+
return mergeParts([{ geometry: cone(0.35, 1.2, 0), color: GRASS }]);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Builds the merged proxy geometry for a scatter item id, memoized per builder call by the caller.
|
|
89
|
+
* @internal — the geometry catalog behind the `InstancedScatter` renderer.
|
|
90
|
+
*/
|
|
91
|
+
export function buildScatterProxy(item) {
|
|
92
|
+
const builder = BUILDERS[item];
|
|
93
|
+
return builder === undefined ? defaultProxy() : builder();
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./scatter/InstancedScatter.js";
|
package/dist/scatter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./scatter/InstancedScatter.js";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { EditorDocument } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
4
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
|
|
7
|
+
* installed (editor host live), subscribe to document patches; otherwise use the prop.
|
|
8
|
+
* Document is authoritative — runtime overrides never land here unless written back.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function useLiveEditorDocument(document: EditorDocument, live?: boolean): EditorDocument;
|
|
12
|
+
/** Props for {@link AuthoredPaths}: the document, the ground field to drape over, and a kind filter. */
|
|
13
|
+
export interface AuthoredPathsProps {
|
|
14
|
+
document: EditorDocument;
|
|
15
|
+
field: TerrainField;
|
|
16
|
+
/** Only render these path kinds; default renders every non-scatter path. */
|
|
17
|
+
kinds?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the
|
|
21
|
+
* editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes
|
|
22
|
+
* from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): import("react").JSX.Element;
|
|
25
|
+
/** Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. */
|
|
26
|
+
export interface AuthoredObjectsProps {
|
|
27
|
+
document: EditorDocument;
|
|
28
|
+
field: TerrainField;
|
|
29
|
+
/** Extra lift in meters on top of each marker's own verticalOffset (default 0). */
|
|
30
|
+
verticalOffset?: number;
|
|
31
|
+
/**
|
|
32
|
+
* How to treat instance ids already in the object store. Default `"keep"` so an onInit
|
|
33
|
+
* `placeAuthoredObjects` call wins and remounts stay remount-safe.
|
|
34
|
+
*/
|
|
35
|
+
onExisting?: "throw" | "replace" | "keep";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
|
|
39
|
+
* WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
|
|
40
|
+
* headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function AuthoredObjects({ document, field, verticalOffset, onExisting, }: AuthoredObjectsProps): null;
|
|
43
|
+
/** Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. */
|
|
44
|
+
export interface AuthoredSceneProps {
|
|
45
|
+
document: EditorDocument;
|
|
46
|
+
field: TerrainField;
|
|
47
|
+
/** Restrict rendered path kinds; default renders every non-scatter path. */
|
|
48
|
+
pathKinds?: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Scatter palette item id → model asset id or `ModelConfig`; a mapped item GPU-instances the real
|
|
51
|
+
* catalog GLB instead of the stylized proxy. Keyed by scatter `item` (not entity name — pass a
|
|
52
|
+
* dedicated map even if it shares entries with `entityModels`). Requires `assets`.
|
|
53
|
+
*/
|
|
54
|
+
scatterModels?: Record<string, string | ModelConfig>;
|
|
55
|
+
/** Catalog `scatterModels` string ids resolve through; required together with `scatterModels`. */
|
|
56
|
+
assets?: AssetCatalog;
|
|
57
|
+
/**
|
|
58
|
+
* When true (default), subscribe to the global document live-sync bus if the editor host is
|
|
59
|
+
* mounted so headless/RPC document patches hot-apply without a full reload. Pass false to pin
|
|
60
|
+
* the `document` prop (tests, one-shot previews).
|
|
61
|
+
*/
|
|
62
|
+
live?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Place the document's catalog-id markers into the object
|
|
65
|
+
* store — WorldScene renders them via the game's `objectModels` seam. Omit when the game places
|
|
66
|
+
* props itself in onInit with `placeAuthoredObjects`.
|
|
67
|
+
*/
|
|
68
|
+
placeObjects?: boolean | {
|
|
69
|
+
verticalOffset?: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one
|
|
74
|
+
* mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor:
|
|
75
|
+
* drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke
|
|
76
|
+
* render code. When a live-sync bus is installed (editor host), document patches stream in and
|
|
77
|
+
* re-render automatically — document is authoritative; runtime overrides stay ephemeral unless
|
|
78
|
+
* written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`);
|
|
79
|
+
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
80
|
+
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
81
|
+
*/
|
|
82
|
+
export declare function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live, placeObjects, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { getDocumentLiveSync, subscribeDocumentLiveSyncInstall, } from "@jgengine/core/editor/index";
|
|
5
|
+
import { placeAuthoredObjects, resolveAuthoredObjects, } from "@jgengine/core/world/authoredObjects";
|
|
6
|
+
import { buildRoadRibbon, roundPathCorners } from "@jgengine/core/world/roads";
|
|
7
|
+
import { isScatterPath, resolveScatter } from "@jgengine/core/world/scatterRegion";
|
|
8
|
+
import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
|
|
9
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
10
|
+
import { createModelMapResolver } from "../render/resolveModel.js";
|
|
11
|
+
import { InstancedScatter } from "../scatter/InstancedScatter.js";
|
|
12
|
+
import { GeneratedAssetInstance } from "./GeneratedAssetRenderer.js";
|
|
13
|
+
import { registerBuiltinSceneKindRenderers } from "./builtinSceneKindRenderers.js";
|
|
14
|
+
import { getSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
|
|
17
|
+
* installed (editor host live), subscribe to document patches; otherwise use the prop.
|
|
18
|
+
* Document is authoritative — runtime overrides never land here unless written back.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export function useLiveEditorDocument(document, live = true) {
|
|
22
|
+
const [resolved, setResolved] = useState(document);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!live) {
|
|
25
|
+
setResolved(document);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let unsubDoc;
|
|
29
|
+
const attach = () => {
|
|
30
|
+
unsubDoc?.();
|
|
31
|
+
unsubDoc = undefined;
|
|
32
|
+
const sync = getDocumentLiveSync();
|
|
33
|
+
if (sync === null) {
|
|
34
|
+
setResolved(document);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
setResolved(sync.getDocument());
|
|
38
|
+
unsubDoc = sync.subscribeDocument((event) => setResolved(event.document));
|
|
39
|
+
};
|
|
40
|
+
attach();
|
|
41
|
+
const unsubInstall = subscribeDocumentLiveSyncInstall(attach);
|
|
42
|
+
return () => {
|
|
43
|
+
unsubInstall();
|
|
44
|
+
unsubDoc?.();
|
|
45
|
+
};
|
|
46
|
+
}, [document, live]);
|
|
47
|
+
return live ? resolved : document;
|
|
48
|
+
}
|
|
49
|
+
registerBuiltinSceneKindRenderers();
|
|
50
|
+
function markerToKindObject(marker) {
|
|
51
|
+
return { id: marker.id, kind: marker.kind, position: marker.position, ...(marker.rotationY === undefined ? {} : { rotationY: marker.rotationY }), ...(marker.meta === undefined ? {} : { meta: marker.meta }) };
|
|
52
|
+
}
|
|
53
|
+
function volumeToKindObject(volume) {
|
|
54
|
+
return {
|
|
55
|
+
id: volume.id,
|
|
56
|
+
kind: volume.kind,
|
|
57
|
+
center: volume.center,
|
|
58
|
+
...(volume.halfExtents === undefined ? {} : { halfExtents: volume.halfExtents }),
|
|
59
|
+
...(volume.radius === undefined ? {} : { radius: volume.radius }),
|
|
60
|
+
...(volume.meta === undefined ? {} : { meta: volume.meta }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function pathToKindObject(path) {
|
|
64
|
+
return { id: path.id, kind: path.kind, points: path.points.map((point) => ({ x: point.x, y: point.y, z: point.z })), ...(path.meta === undefined ? {} : { meta: path.meta }) };
|
|
65
|
+
}
|
|
66
|
+
/** Renders every marker placed as a parametric generator asset, re-resolved from its `meta` + seed. */
|
|
67
|
+
function AuthoredGenerators({ document, field }) {
|
|
68
|
+
const markers = useMemo(() => document.markers.filter((marker) => typeof marker.meta?.["assetId"] === "string" && getAssetGenerator(marker.meta["assetId"]) !== undefined), [document.markers]);
|
|
69
|
+
return (_jsx(_Fragment, { children: markers.map((marker) => (_jsx(GeneratedAssetInstance, { meta: marker.meta, position: [marker.position.x, field.sampleHeight(marker.position.x, marker.position.z), marker.position.z], rotationY: marker.rotationY ?? 0 }, marker.id))) }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mounts every registered studio renderer for the kinds present in a document — water and any
|
|
73
|
+
* game/example-registered kind — grouping objects by kind and handing each renderer its slice.
|
|
74
|
+
* Scatter stays on the batched {@link InstancedScatter} path (its cross-region avoid masks need the
|
|
75
|
+
* whole document), so it is skipped here.
|
|
76
|
+
*/
|
|
77
|
+
function AuthoredStudios({ document, context }) {
|
|
78
|
+
const byKind = useMemo(() => {
|
|
79
|
+
const groups = new Map();
|
|
80
|
+
const add = (kind, object) => {
|
|
81
|
+
if (isScatterPath({ kind }) || getSceneKindRenderer(kind) === undefined)
|
|
82
|
+
return;
|
|
83
|
+
const bucket = groups.get(kind);
|
|
84
|
+
if (bucket === undefined)
|
|
85
|
+
groups.set(kind, [object]);
|
|
86
|
+
else
|
|
87
|
+
bucket.push(object);
|
|
88
|
+
};
|
|
89
|
+
for (const marker of document.markers)
|
|
90
|
+
add(marker.kind, markerToKindObject(marker));
|
|
91
|
+
for (const volume of document.volumes)
|
|
92
|
+
add(volume.kind, volumeToKindObject(volume));
|
|
93
|
+
for (const path of document.paths)
|
|
94
|
+
add(path.kind, pathToKindObject(path));
|
|
95
|
+
return groups;
|
|
96
|
+
}, [document]);
|
|
97
|
+
return (_jsx(_Fragment, { children: [...byKind.entries()].map(([kind, objects]) => {
|
|
98
|
+
const renderer = getSceneKindRenderer(kind);
|
|
99
|
+
return renderer === undefined ? null : _jsx(Fragment, { children: renderer({ objects, context }) }, kind);
|
|
100
|
+
}) }));
|
|
101
|
+
}
|
|
102
|
+
const DEFAULT_PATH_COLOR = "#7a6444";
|
|
103
|
+
/** One editor path drawn as a ground-draped ribbon — subdivided and sampled against the live field. */
|
|
104
|
+
function DrapedPath({ points, width, color, field, }) {
|
|
105
|
+
const geometry = useMemo(() => {
|
|
106
|
+
// Fillet sharp turns first so bends read as smooth arcs, not overlapping rectangles.
|
|
107
|
+
const rounded = roundPathCorners(points, Math.max(1.5, width * 0.9), 6);
|
|
108
|
+
const ribbon = buildRoadRibbon(rounded, width, (x, z) => field.sampleHeight(x, z), {
|
|
109
|
+
elevation: 0.18,
|
|
110
|
+
maxSegmentLength: 1,
|
|
111
|
+
});
|
|
112
|
+
const geo = new THREE.BufferGeometry();
|
|
113
|
+
geo.setAttribute("position", new THREE.BufferAttribute(ribbon.positions, 3));
|
|
114
|
+
geo.setIndex(new THREE.BufferAttribute(ribbon.indices, 1));
|
|
115
|
+
geo.computeVertexNormals();
|
|
116
|
+
return geo;
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
}, [points, width, field]);
|
|
119
|
+
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
120
|
+
if (points.length < 2)
|
|
121
|
+
return null;
|
|
122
|
+
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 1, metalness: 0, side: THREE.DoubleSide }) }));
|
|
123
|
+
}
|
|
124
|
+
function pathColor(meta, color) {
|
|
125
|
+
if (typeof meta?.["color"] === "string")
|
|
126
|
+
return meta["color"];
|
|
127
|
+
return color ?? DEFAULT_PATH_COLOR;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the
|
|
131
|
+
* editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes
|
|
132
|
+
* from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes.
|
|
133
|
+
*/
|
|
134
|
+
export function AuthoredPaths({ document, field, kinds }) {
|
|
135
|
+
const paths = document.paths.filter((path) => !isScatterPath(path) && getSceneKindRenderer(path.kind) === undefined && (kinds === undefined || kinds.includes(path.kind)));
|
|
136
|
+
return (_jsx(_Fragment, { children: paths.map((path) => (_jsx(DrapedPath, { points: path.points.map((point) => [point.x, point.z]), width: path.width ?? 4, color: pathColor(path.meta, path.color), field: field }, path.id))) }));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
|
|
140
|
+
* WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
|
|
141
|
+
* headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
|
|
142
|
+
*/
|
|
143
|
+
export function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", }) {
|
|
144
|
+
const ctx = useGameContext();
|
|
145
|
+
const objects = useMemo(() => resolveAuthoredObjects(document), [document]);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (objects.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
placeAuthoredObjects(ctx.scene.object, objects, (x, z) => field.sampleHeight(x, z), {
|
|
150
|
+
verticalOffset,
|
|
151
|
+
onExisting,
|
|
152
|
+
});
|
|
153
|
+
}, [ctx.scene.object, field, objects, onExisting, verticalOffset]);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one
|
|
158
|
+
* mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor:
|
|
159
|
+
* drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke
|
|
160
|
+
* render code. When a live-sync bus is installed (editor host), document patches stream in and
|
|
161
|
+
* re-render automatically — document is authoritative; runtime overrides stay ephemeral unless
|
|
162
|
+
* written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`);
|
|
163
|
+
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
164
|
+
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
165
|
+
*/
|
|
166
|
+
export function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }) {
|
|
167
|
+
const liveDocument = useLiveEditorDocument(document, live);
|
|
168
|
+
const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]);
|
|
169
|
+
const resolveItem = useMemo(() => createModelMapResolver(scatterModels, assets, "scatterModels"), [scatterModels, assets]);
|
|
170
|
+
const shouldPlaceObjects = placeObjects === true || (typeof placeObjects === "object" && placeObjects !== null);
|
|
171
|
+
const objectVerticalOffset = typeof placeObjects === "object" && placeObjects !== null ? (placeObjects.verticalOffset ?? 0) : 0;
|
|
172
|
+
return (_jsxs(_Fragment, { children: [_jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: { document: liveDocument, field, ...(assets === undefined ? {} : { assets }) } }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset })) : null] }));
|
|
173
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Props for {@link GeneratedAsset}: the placed instance's `meta` (assetId + params + seed) and transform. */
|
|
2
|
+
export interface GeneratedAssetProps {
|
|
3
|
+
/** The placed instance meta — `{ assetId, seed, ...params }`. Re-resolved every render, never baked. */
|
|
4
|
+
meta: Record<string, unknown> | undefined;
|
|
5
|
+
position?: readonly [number, number, number];
|
|
6
|
+
rotationY?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders one placed generator-asset instance by re-resolving its `meta` through the registered
|
|
10
|
+
* generator and instancing the parts. Returns null when the meta names no generator. The runtime
|
|
11
|
+
* counterpart to placing a parametric asset — the geometry is data, recomputed from params + seed.
|
|
12
|
+
* @internal — `AuthoredScene` mounts this for generator markers automatically.
|
|
13
|
+
*/
|
|
14
|
+
export declare function GeneratedAssetInstance({ meta, position, rotationY }: GeneratedAssetProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { resolveGeneratorAsset } from "@jgengine/core/scene/assetGenerator";
|
|
5
|
+
const UNIT_BOX = new THREE.BoxGeometry(1, 1, 1);
|
|
6
|
+
const NEUTRAL = "#b8b0a4";
|
|
7
|
+
/** All parts of one generated asset, batched into one instanced box draw per color. */
|
|
8
|
+
function GeneratedParts({ asset }) {
|
|
9
|
+
const byColor = useMemo(() => {
|
|
10
|
+
const groups = new Map();
|
|
11
|
+
for (const part of asset.parts) {
|
|
12
|
+
const color = part.color ?? NEUTRAL;
|
|
13
|
+
const bucket = groups.get(color);
|
|
14
|
+
if (bucket === undefined)
|
|
15
|
+
groups.set(color, [part]);
|
|
16
|
+
else
|
|
17
|
+
bucket.push(part);
|
|
18
|
+
}
|
|
19
|
+
return [...groups.entries()];
|
|
20
|
+
}, [asset]);
|
|
21
|
+
return (_jsx(_Fragment, { children: byColor.map(([color, parts]) => (_jsx(ColorBatch, { color: color, parts: parts }, color))) }));
|
|
22
|
+
}
|
|
23
|
+
function ColorBatch({ color, parts }) {
|
|
24
|
+
const meshRef = useRef(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const mesh = meshRef.current;
|
|
27
|
+
if (mesh === null)
|
|
28
|
+
return;
|
|
29
|
+
const matrix = new THREE.Matrix4();
|
|
30
|
+
const position = new THREE.Vector3();
|
|
31
|
+
const quaternion = new THREE.Quaternion();
|
|
32
|
+
const euler = new THREE.Euler();
|
|
33
|
+
const scale = new THREE.Vector3();
|
|
34
|
+
parts.forEach((part, index) => {
|
|
35
|
+
position.set(part.position[0], part.position[1], part.position[2]);
|
|
36
|
+
euler.set(0, part.rotationY ?? 0, 0);
|
|
37
|
+
quaternion.setFromEuler(euler);
|
|
38
|
+
scale.set(Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2]));
|
|
39
|
+
matrix.compose(position, quaternion, scale);
|
|
40
|
+
mesh.setMatrixAt(index, matrix);
|
|
41
|
+
});
|
|
42
|
+
mesh.count = parts.length;
|
|
43
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
44
|
+
mesh.computeBoundingSphere();
|
|
45
|
+
}, [parts]);
|
|
46
|
+
return (_jsx("instancedMesh", { ref: meshRef, args: [UNIT_BOX, undefined, Math.max(1, parts.length)], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 0.62, metalness: 0.04 }) }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Renders one placed generator-asset instance by re-resolving its `meta` through the registered
|
|
50
|
+
* generator and instancing the parts. Returns null when the meta names no generator. The runtime
|
|
51
|
+
* counterpart to placing a parametric asset — the geometry is data, recomputed from params + seed.
|
|
52
|
+
* @internal — `AuthoredScene` mounts this for generator markers automatically.
|
|
53
|
+
*/
|
|
54
|
+
export function GeneratedAssetInstance({ meta, position = [0, 0, 0], rotationY = 0 }) {
|
|
55
|
+
const asset = useMemo(() => resolveGeneratorAsset(meta), [meta]);
|
|
56
|
+
if (asset === null)
|
|
57
|
+
return null;
|
|
58
|
+
return (_jsx("group", { position: [position[0], position[1], position[2]], rotation: [0, rotationY, 0], children: _jsx(GeneratedParts, { asset: asset }) }));
|
|
59
|
+
}
|