@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.
Files changed (234) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/GamePhaseStamp.d.ts +1 -0
  5. package/dist/GamePhaseStamp.js +9 -0
  6. package/dist/GamePlayerShell.d.ts +0 -12
  7. package/dist/GamePlayerShell.js +88 -1043
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.js +30 -8
  10. package/dist/audio/audioWire.d.ts +22 -0
  11. package/dist/audio/audioWire.js +19 -0
  12. package/dist/behaviour.d.ts +2 -1
  13. package/dist/behaviour.js +2 -1
  14. package/dist/behaviourAttach.d.ts +1 -0
  15. package/dist/behaviourAttach.js +1 -0
  16. package/dist/behaviourDriver.d.ts +1 -0
  17. package/dist/behaviourDriver.js +1 -0
  18. package/dist/boundActionDispatch.d.ts +13 -0
  19. package/dist/boundActionDispatch.js +37 -0
  20. package/dist/camera/GameCameraRig.d.ts +5 -2
  21. package/dist/camera/GameCameraRig.js +2 -2
  22. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  23. package/dist/camera/GameFirstPersonCamera.js +8 -3
  24. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  25. package/dist/camera/GameInspectionCamera.js +2 -2
  26. package/dist/camera/GameOrbitCamera.js +24 -9
  27. package/dist/camera/cameraBlendMath.d.ts +3 -0
  28. package/dist/camera/cameraBlendMath.js +3 -0
  29. package/dist/camera/fovPreference.d.ts +6 -1
  30. package/dist/camera/fovPreference.js +6 -1
  31. package/dist/camera/index.d.ts +4 -7
  32. package/dist/camera/index.js +4 -7
  33. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  34. package/dist/camera/inspectionCameraMath.js +7 -2
  35. package/dist/camera/orbitCameraMath.d.ts +36 -6
  36. package/dist/camera/orbitCameraMath.js +56 -6
  37. package/dist/camera/rigMath.d.ts +94 -24
  38. package/dist/camera/rigMath.js +100 -24
  39. package/dist/camera/rigResolve.d.ts +4 -2
  40. package/dist/camera/rigResolve.js +4 -2
  41. package/dist/camera/shakeChannel.d.ts +6 -2
  42. package/dist/camera/shakeChannel.js +6 -2
  43. package/dist/camera/shakeChannelMath.d.ts +1 -0
  44. package/dist/camera/shakeChannelMath.js +1 -0
  45. package/dist/cartridge/runtime.d.ts +15 -0
  46. package/dist/cartridge/runtime.js +556 -0
  47. package/dist/cartridge/spec.d.ts +188 -0
  48. package/dist/cartridge/spec.js +19 -0
  49. package/dist/cartridge/validate.d.ts +2 -0
  50. package/dist/cartridge/validate.js +78 -0
  51. package/dist/cartridge.d.ts +1 -1
  52. package/dist/cartridge.js +3 -3
  53. package/dist/commandSink.d.ts +8 -3
  54. package/dist/commandSink.js +8 -3
  55. package/dist/defineGame.js +25 -6
  56. package/dist/devtools/ColPanel.d.ts +1 -0
  57. package/dist/devtools/ColPanel.js +14 -0
  58. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  59. package/dist/devtools/DevtoolsOverlay.js +19 -450
  60. package/dist/devtools/KeysPanel.d.ts +4 -0
  61. package/dist/devtools/KeysPanel.js +31 -0
  62. package/dist/devtools/LogsPanel.d.ts +1 -0
  63. package/dist/devtools/LogsPanel.js +12 -0
  64. package/dist/devtools/NetPanel.d.ts +4 -0
  65. package/dist/devtools/NetPanel.js +10 -0
  66. package/dist/devtools/PerfPanel.d.ts +4 -0
  67. package/dist/devtools/PerfPanel.js +53 -0
  68. package/dist/devtools/TunePanel.d.ts +3 -0
  69. package/dist/devtools/TunePanel.js +309 -0
  70. package/dist/devtools/agentBridge.d.ts +41 -0
  71. package/dist/devtools/agentBridge.js +262 -0
  72. package/dist/devtools/collisionDebug.d.ts +6 -0
  73. package/dist/devtools/collisionDebug.js +6 -0
  74. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  75. package/dist/devtools/collisionDebugMath.js +24 -5
  76. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  77. package/dist/devtools/devtoolsOverrides.js +48 -0
  78. package/dist/devtools/panelAtoms.d.ts +9 -0
  79. package/dist/devtools/panelAtoms.js +10 -0
  80. package/dist/devtools/perfDiagnose.d.ts +2 -0
  81. package/dist/devtools/perfDiagnose.js +43 -0
  82. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  83. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  84. package/dist/drivers/FrameDriver.d.ts +37 -0
  85. package/dist/drivers/FrameDriver.js +232 -0
  86. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  87. package/dist/drivers/HudOnlyDriver.js +60 -0
  88. package/dist/environment/Daylight.d.ts +23 -7
  89. package/dist/environment/Daylight.js +106 -13
  90. package/dist/environment/EnvironmentScene.js +32 -6
  91. package/dist/environment/VolumetricClouds.d.ts +17 -0
  92. package/dist/environment/VolumetricClouds.js +202 -0
  93. package/dist/environment/daylightCycle.d.ts +3 -1
  94. package/dist/environment/daylightCycle.js +3 -1
  95. package/dist/environment/groundPadMath.d.ts +3 -0
  96. package/dist/environment/groundPadMath.js +3 -0
  97. package/dist/environment/index.d.ts +1 -0
  98. package/dist/environment/index.js +1 -0
  99. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  100. package/dist/environment/skyLightingPolicy.js +2 -0
  101. package/dist/hotbarActions.d.ts +26 -0
  102. package/dist/hotbarActions.js +27 -0
  103. package/dist/inputSink.d.ts +16 -4
  104. package/dist/inputSink.js +57 -7
  105. package/dist/materialOverride.d.ts +10 -0
  106. package/dist/materialOverride.js +15 -3
  107. package/dist/multiplayer.js +8 -2
  108. package/dist/postfx/PostProcessing.js +47 -9
  109. package/dist/postfx/gradeShader.js +9 -1
  110. package/dist/registry.d.ts +3 -2
  111. package/dist/render/EnvironmentLighting.d.ts +10 -0
  112. package/dist/render/EnvironmentLighting.js +34 -0
  113. package/dist/render/SceneLighting.d.ts +7 -0
  114. package/dist/render/SceneLighting.js +18 -0
  115. package/dist/render/SceneModels.d.ts +14 -0
  116. package/dist/render/SceneModels.js +337 -0
  117. package/dist/render/defaultSurface.d.ts +29 -0
  118. package/dist/render/defaultSurface.js +101 -0
  119. package/dist/render/modelLoad.d.ts +24 -0
  120. package/dist/render/modelLoad.js +73 -0
  121. package/dist/render/modelRender.d.ts +11 -0
  122. package/dist/render/modelRender.js +15 -0
  123. package/dist/render/resolveModel.d.ts +39 -3
  124. package/dist/render/resolveModel.js +79 -2
  125. package/dist/render/useEntityRenderCues.d.ts +13 -0
  126. package/dist/render/useEntityRenderCues.js +62 -0
  127. package/dist/scatter/InstancedScatter.d.ts +24 -0
  128. package/dist/scatter/InstancedScatter.js +130 -0
  129. package/dist/scatter/scatterModels.d.ts +25 -0
  130. package/dist/scatter/scatterModels.js +61 -0
  131. package/dist/scatter/scatterProxies.d.ts +6 -0
  132. package/dist/scatter/scatterProxies.js +94 -0
  133. package/dist/scatter.d.ts +1 -0
  134. package/dist/scatter.js +1 -0
  135. package/dist/scene/AuthoredScene.d.ts +82 -0
  136. package/dist/scene/AuthoredScene.js +173 -0
  137. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  138. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  139. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  140. package/dist/scene/GrassFieldRenderer.js +25 -0
  141. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  142. package/dist/scene/SoilPatchRenderer.js +21 -0
  143. package/dist/scene/StudioStage.d.ts +37 -0
  144. package/dist/scene/StudioStage.js +42 -0
  145. package/dist/scene/WaterRenderer.d.ts +2 -0
  146. package/dist/scene/WaterRenderer.js +37 -0
  147. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  148. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  149. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  150. package/dist/scene/sceneKindRenderers.js +17 -0
  151. package/dist/scene.d.ts +1 -0
  152. package/dist/scene.js +1 -0
  153. package/dist/settings/appliedSettings.d.ts +5 -0
  154. package/dist/settings/appliedSettings.js +9 -0
  155. package/dist/settings/settingsController.d.ts +2 -0
  156. package/dist/settings/settingsController.js +22 -1
  157. package/dist/shellConstants.d.ts +4 -0
  158. package/dist/shellConstants.js +4 -0
  159. package/dist/shellMovement.d.ts +8 -0
  160. package/dist/shellMovement.js +11 -0
  161. package/dist/shellPointer.d.ts +20 -0
  162. package/dist/shellPointer.js +28 -0
  163. package/dist/structures/PlacementGhost.d.ts +4 -0
  164. package/dist/structures/PlacementGhost.js +4 -0
  165. package/dist/structures/TransformGizmo.d.ts +43 -0
  166. package/dist/structures/TransformGizmo.js +55 -0
  167. package/dist/structures/index.d.ts +1 -0
  168. package/dist/structures/index.js +1 -0
  169. package/dist/terrain/GrassField.js +1 -1
  170. package/dist/terrain/TerraformBrushCursor.js +3 -0
  171. package/dist/terrain/grassBudget.d.ts +11 -2
  172. package/dist/terrain/grassBudget.js +20 -4
  173. package/dist/terrain/grassGeometry.d.ts +3 -0
  174. package/dist/terrain/grassGeometry.js +3 -0
  175. package/dist/terrain/grassMaterial.d.ts +2 -0
  176. package/dist/terrain/grassMaterial.js +2 -0
  177. package/dist/terrain/random.d.ts +3 -0
  178. package/dist/terrain/random.js +3 -0
  179. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  180. package/dist/terrain/soilPatchMaterial.js +109 -0
  181. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  182. package/dist/terrain/terrainDetailMaterial.js +72 -5
  183. package/dist/terrain/terrainMath.d.ts +8 -1
  184. package/dist/terrain/terrainMath.js +8 -1
  185. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  186. package/dist/touch/TouchControlsOverlay.js +201 -22
  187. package/dist/useShellMultiplayerSync.d.ts +9 -0
  188. package/dist/useShellMultiplayerSync.js +98 -0
  189. package/dist/visibility/CullingProvider.js +1 -1
  190. package/dist/water/OceanConfig.d.ts +2 -0
  191. package/dist/water/OceanConfig.js +2 -0
  192. package/dist/weather/fireSpreadPose.d.ts +1 -0
  193. package/dist/weather/fireSpreadPose.js +1 -0
  194. package/dist/weather/weatherGeometry.d.ts +1 -0
  195. package/dist/weather/weatherGeometry.js +1 -0
  196. package/dist/weather/weatherMath.d.ts +3 -0
  197. package/dist/weather/weatherMath.js +3 -0
  198. package/dist/weather/weatherUniforms.d.ts +3 -0
  199. package/dist/weather/weatherUniforms.js +3 -0
  200. package/dist/world/RetainedVfx.d.ts +10 -0
  201. package/dist/world/RetainedVfx.js +136 -0
  202. package/dist/world/WorldHud.d.ts +49 -2
  203. package/dist/world/WorldHud.js +87 -2
  204. package/dist/world/WorldScene.d.ts +21 -0
  205. package/dist/world/WorldScene.js +159 -0
  206. package/dist/world/WorldVfx.d.ts +9 -0
  207. package/dist/world/WorldVfx.js +184 -0
  208. package/dist/world/entityPose.d.ts +2 -0
  209. package/dist/world/entityPose.js +2 -0
  210. package/dist/world/floatTextStyle.d.ts +1 -0
  211. package/dist/world/floatTextStyle.js +1 -0
  212. package/dist/world/telegraphPulse.d.ts +1 -0
  213. package/dist/world/telegraphPulse.js +1 -0
  214. package/dist/world/worldBarSamples.d.ts +25 -0
  215. package/dist/world/worldBarSamples.js +38 -0
  216. package/dist/worldSky.d.ts +5 -0
  217. package/dist/worldSky.js +11 -0
  218. package/dist/worldSync.d.ts +2 -1
  219. package/dist/worldSync.js +2 -1
  220. package/package.json +6 -6
  221. package/skills/jgengine/SKILL.md +51 -0
  222. package/skills/jgengine/api.md +434 -0
  223. package/skills/jgengine/capabilities.md +29 -0
  224. package/skills/jgengine/reference-cartridge.md +72 -0
  225. package/skills/jgengine-ui/SKILL.md +36 -0
  226. package/skills/jgengine-ui/api.md +1852 -0
  227. package/skills/jgengine-ui/capabilities.md +142 -0
  228. package/skills/jgengine-ui/reference.md +500 -0
  229. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  230. package/skills/jgengine-world/SKILL.md +38 -0
  231. package/skills/jgengine-world/api.md +2652 -0
  232. package/skills/jgengine-world/capabilities.md +370 -0
  233. package/skills/jgengine-world/reference.md +222 -0
  234. 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";
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ /** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
2
+ export declare function registerGrassFieldRenderer(): void;