@jgengine/shell 0.10.0 → 0.12.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 (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -1,23 +1,34 @@
1
1
  import * as THREE from "three";
2
2
  import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
3
3
  export declare const PAINT_TEXTURE_SIZE = 512;
4
+ /** @internal */
4
5
  export declare function cloneModelScene(source: THREE.Object3D, options?: {
5
6
  cloneMaterials?: boolean;
6
7
  }): THREE.Object3D;
8
+ /** @internal */
7
9
  export declare function disposeClonedMaterials(root: THREE.Object3D): void;
10
+ /** @internal */
8
11
  export declare function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[];
9
12
  export interface MaterialCache {
10
13
  materials: THREE.MeshStandardMaterial[];
11
14
  seedColor: THREE.Color;
12
15
  }
16
+ /** @internal */
13
17
  export declare function cacheStandardMaterials(root: THREE.Object3D, into?: MaterialCache | null): MaterialCache;
18
+ /** @internal */
14
19
  export declare function applyPaintTextureToMaterials(materials: readonly THREE.MeshStandardMaterial[], paint: PaintCanvas): void;
15
20
  export interface PaintCanvas {
16
21
  canvas: HTMLCanvasElement;
17
22
  context: CanvasRenderingContext2D;
18
23
  texture: THREE.CanvasTexture;
19
24
  }
25
+ /** @internal */
20
26
  export declare function createPaintCanvas(seed: THREE.MeshStandardMaterial, size?: number): PaintCanvas;
27
+ /** @internal */
28
+ export declare function disposePaintCanvas(paint: PaintCanvas): void;
29
+ /** @internal */
21
30
  export declare function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void;
31
+ /** @internal */
22
32
  export declare function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void;
33
+ /** @internal */
23
34
  export declare function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number;
@@ -1,6 +1,7 @@
1
1
  import * as THREE from "three";
2
2
  import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
3
3
  export const PAINT_TEXTURE_SIZE = 512;
4
+ /** @internal */
4
5
  export function cloneModelScene(source, options) {
5
6
  const clone = cloneSkinned(source);
6
7
  if (options?.cloneMaterials === false)
@@ -15,6 +16,7 @@ export function cloneModelScene(source, options) {
15
16
  });
16
17
  return clone;
17
18
  }
19
+ /** @internal */
18
20
  export function disposeClonedMaterials(root) {
19
21
  const seen = new Set();
20
22
  root.traverse((node) => {
@@ -33,6 +35,7 @@ export function disposeClonedMaterials(root) {
33
35
  function isMeshStandardMaterial(material) {
34
36
  return material.isMeshStandardMaterial === true;
35
37
  }
38
+ /** @internal */
36
39
  export function standardMaterialsOf(root) {
37
40
  const materials = [];
38
41
  root.traverse((node) => {
@@ -46,6 +49,7 @@ export function standardMaterialsOf(root) {
46
49
  });
47
50
  return materials;
48
51
  }
52
+ /** @internal */
49
53
  export function cacheStandardMaterials(root, into) {
50
54
  if (into !== null && into !== undefined)
51
55
  return into;
@@ -53,12 +57,14 @@ export function cacheStandardMaterials(root, into) {
53
57
  const seedColor = materials[0]?.color.clone() ?? new THREE.Color("#ffffff");
54
58
  return { materials, seedColor };
55
59
  }
60
+ /** @internal */
56
61
  export function applyPaintTextureToMaterials(materials, paint) {
57
62
  for (const material of materials) {
58
63
  material.map = paint.texture;
59
64
  material.needsUpdate = true;
60
65
  }
61
66
  }
67
+ /** @internal */
62
68
  export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
63
69
  const canvas = document.createElement("canvas");
64
70
  canvas.width = size;
@@ -76,6 +82,13 @@ export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
76
82
  texture.colorSpace = seed.map?.colorSpace ?? THREE.SRGBColorSpace;
77
83
  return { canvas, context, texture };
78
84
  }
85
+ /** @internal */
86
+ export function disposePaintCanvas(paint) {
87
+ paint.texture.dispose();
88
+ paint.canvas.width = 0;
89
+ paint.canvas.height = 0;
90
+ }
91
+ /** @internal */
79
92
  export function drawPaintStrokes(paint, strokes) {
80
93
  const { canvas, context, texture } = paint;
81
94
  for (const stroke of strokes) {
@@ -86,9 +99,11 @@ export function drawPaintStrokes(paint, strokes) {
86
99
  }
87
100
  texture.needsUpdate = true;
88
101
  }
102
+ /** @internal */
89
103
  export function applyPaintTexture(root, paint) {
90
104
  applyPaintTextureToMaterials(standardMaterialsOf(root), paint);
91
105
  }
106
+ /** @internal */
92
107
  export function syncPaintCanvas(paint, seedColor, strokes, drawnCount) {
93
108
  if (strokes.length < drawnCount) {
94
109
  const { canvas, context } = paint;
@@ -1,14 +1,65 @@
1
1
  import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
2
2
  import type { ModelConfig } from "@jgengine/core/game/playableGame";
3
+ import type { GameContextModels } from "@jgengine/core/runtime/gameContext";
3
4
  export interface ModelResolveContext {
4
- seam: "entityModels" | "objectModels";
5
+ seam: "entityModels" | "objectModels" | "scatterModels";
5
6
  key: string;
6
7
  }
7
8
  /**
8
9
  * Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
9
10
  * ids throw — silent generic-primitive fallback only happens when the mapping
10
11
  * omits the key entirely (or uses tryResolveCatalogModel for optional ids).
11
- */
12
+ * @internal
13
+ */
12
14
  export declare function resolveModel(value: string | ModelConfig | undefined, assets: AssetCatalog, context?: ModelResolveContext): ModelConfig | undefined;
13
- /** Soft lookup used when an object catalog id may double as a model asset id. */
15
+ /** Soft lookup used when an object catalog id may double as a model asset id.
16
+ * @internal
17
+ */
14
18
  export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
19
+ /**
20
+ * Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
21
+ * so mounts receive fully-resolved `ModelConfig`s.
22
+ * @internal
23
+ */
24
+ export declare function resolveEntityModel(value: string | ModelConfig | undefined, assets: AssetCatalog, key: string): ModelConfig | undefined;
25
+ /**
26
+ * Preferred + optional fallback catalog ids for a single entity/object slot.
27
+ * Soft-resolves through the catalog: when neither id is live (pack not pulled/
28
+ * reindexed yet), the mapping is omitted and the shell keeps its primitive.
29
+ * Re-home later by fixing ids / pulling packs — no Kenney, no hard throws.
30
+ */
31
+ export type ModelPick = {
32
+ model?: string;
33
+ fallbackModel?: string;
34
+ style?: Omit<ModelConfig, "url" | "dims">;
35
+ };
36
+ /** Soft-resolve `model`, then `fallbackModel`; merge `style` when either hits.
37
+ * @internal
38
+ */
39
+ export declare function pickModel(assets: AssetCatalog, pick: ModelPick): ModelConfig | undefined;
40
+ /** Build an entityModels/objectModels map from a plan; missing catalog ids drop out (primitive).
41
+ * @internal
42
+ */
43
+ export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<string, ModelPick>): Record<string, ModelConfig>;
44
+ /**
45
+ * The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
46
+ * object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
47
+ * ids (a missing catalog id keeps the default collider instead of throwing — the render path already
48
+ * reports it), passes inline configs through. `undefined` when the playable maps no models at all.
49
+ * @internal
50
+ */
51
+ export declare function contextModels(playable: {
52
+ game: {
53
+ assets: AssetCatalog;
54
+ };
55
+ entityModels?: Record<string, string | ModelConfig>;
56
+ objectModels?: Record<string, string | ModelConfig>;
57
+ }): GameContextModels | undefined;
58
+ /**
59
+ * Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
60
+ * `undefined` when either half is missing so the caller can fall back to its own default renderer.
61
+ * An unmapped key resolves to `null` (deliberate fallback); a mapped key with a missing/misspelled
62
+ * catalog id throws via {@link resolveModel}, same as the `entityModels`/`objectModels` seams.
63
+ * @internal
64
+ */
65
+ export declare function createModelMapResolver(map: Record<string, string | ModelConfig> | undefined, assets: AssetCatalog | undefined, seam: ModelResolveContext["seam"]): ((key: string) => ModelConfig | null) | undefined;
@@ -2,7 +2,8 @@
2
2
  * Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
3
3
  * ids throw — silent generic-primitive fallback only happens when the mapping
4
4
  * omits the key entirely (or uses tryResolveCatalogModel for optional ids).
5
- */
5
+ * @internal
6
+ */
6
7
  export function resolveModel(value, assets, context) {
7
8
  if (value === undefined)
8
9
  return undefined;
@@ -13,12 +14,123 @@ export function resolveModel(value, assets, context) {
13
14
  const where = context === undefined ? `model asset "${value}"` : `${context.seam}["${context.key}"] → "${value}"`;
14
15
  throw new Error(`[jgengine] missing ${where} — not in the asset catalog. Fix the id, register it, or omit the mapping to keep the deliberate primitive/sprite fallback.`);
15
16
  }
16
- return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
17
+ return {
18
+ url: ref.url,
19
+ ...(ref.dims === undefined ? {} : { dims: ref.dims }),
20
+ ...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
21
+ };
17
22
  }
18
- /** Soft lookup used when an object catalog id may double as a model asset id. */
23
+ /** Soft lookup used when an object catalog id may double as a model asset id.
24
+ * @internal
25
+ */
19
26
  export function tryResolveCatalogModel(id, assets) {
20
27
  const ref = assets.resolve(id);
21
28
  if (ref === null)
22
29
  return undefined;
23
- return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
30
+ return {
31
+ url: ref.url,
32
+ ...(ref.dims === undefined ? {} : { dims: ref.dims }),
33
+ ...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
34
+ };
35
+ }
36
+ /**
37
+ * Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
38
+ * so mounts receive fully-resolved `ModelConfig`s.
39
+ * @internal
40
+ */
41
+ export function resolveEntityModel(value, assets, key) {
42
+ const model = resolveModel(value, assets, { seam: "entityModels", key });
43
+ if (model === undefined)
44
+ return model;
45
+ if (model.attachments === undefined && model.parts === undefined)
46
+ return model;
47
+ return {
48
+ ...model,
49
+ ...(model.attachments === undefined
50
+ ? {}
51
+ : {
52
+ attachments: model.attachments.map((attachment) => ({
53
+ ...attachment,
54
+ model: resolveModel(attachment.model, assets) ?? attachment.model,
55
+ })),
56
+ }),
57
+ ...(model.parts === undefined
58
+ ? {}
59
+ : {
60
+ parts: model.parts.map((part) => ({
61
+ ...part,
62
+ model: resolveModel(part.model, assets) ?? part.model,
63
+ })),
64
+ }),
65
+ };
66
+ }
67
+ /** Soft-resolve `model`, then `fallbackModel`; merge `style` when either hits.
68
+ * @internal
69
+ */
70
+ export function pickModel(assets, pick) {
71
+ for (const id of [pick.model, pick.fallbackModel]) {
72
+ if (id === undefined)
73
+ continue;
74
+ const resolved = tryResolveCatalogModel(id, assets);
75
+ if (resolved === undefined)
76
+ continue;
77
+ return pick.style === undefined ? resolved : { ...resolved, ...pick.style };
78
+ }
79
+ return undefined;
80
+ }
81
+ /** Build an entityModels/objectModels map from a plan; missing catalog ids drop out (primitive).
82
+ * @internal
83
+ */
84
+ export function resolveModelPlan(assets, plan) {
85
+ const out = {};
86
+ for (const [key, pick] of Object.entries(plan)) {
87
+ const model = pickModel(assets, pick);
88
+ if (model !== undefined)
89
+ out[key] = model;
90
+ }
91
+ return out;
92
+ }
93
+ function softModelLookup(map, assets) {
94
+ if (map === undefined)
95
+ return undefined;
96
+ return (key) => {
97
+ const value = map[key];
98
+ if (value === undefined || typeof value !== "string")
99
+ return value;
100
+ return tryResolveCatalogModel(value, assets);
101
+ };
102
+ }
103
+ /**
104
+ * The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
105
+ * object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
106
+ * ids (a missing catalog id keeps the default collider instead of throwing — the render path already
107
+ * reports it), passes inline configs through. `undefined` when the playable maps no models at all.
108
+ * @internal
109
+ */
110
+ export function contextModels(playable) {
111
+ const entity = softModelLookup(playable.entityModels, playable.game.assets);
112
+ const object = softModelLookup(playable.objectModels, playable.game.assets);
113
+ if (entity === undefined && object === undefined)
114
+ return undefined;
115
+ return {
116
+ ...(entity === undefined ? {} : { entity }),
117
+ ...(object === undefined ? {} : { object }),
118
+ };
119
+ }
120
+ /**
121
+ * Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
122
+ * `undefined` when either half is missing so the caller can fall back to its own default renderer.
123
+ * An unmapped key resolves to `null` (deliberate fallback); a mapped key with a missing/misspelled
124
+ * catalog id throws via {@link resolveModel}, same as the `entityModels`/`objectModels` seams.
125
+ * @internal
126
+ */
127
+ export function createModelMapResolver(map, assets, seam) {
128
+ if (map === undefined || assets === undefined)
129
+ return undefined;
130
+ return (key) => {
131
+ const mapped = map[key];
132
+ if (mapped === undefined)
133
+ return null;
134
+ return resolveModel(mapped, assets, { seam, key }) ?? null;
135
+ };
24
136
  }
@@ -0,0 +1,13 @@
1
+ import { type MutableRefObject } from "react";
2
+ import { type EntityRenderCues, type RenderCueTuning } from "@jgengine/core/combat/renderCues";
3
+ /**
4
+ * Live motion + animation cues for one entity, read from a mutable ref inside
5
+ * your own `useFrame` — no re-render per frame, no diffing the parent group's
6
+ * position, no game-side module map for attack/hit timing. Backs both custom
7
+ * `renderEntity` rigs and a custom first-person viewmodel (#542): call it with
8
+ * `entity.id` / the local player's userId and drive bob/recoil/reload poses
9
+ * from `cuesRef.current` inside the calling component's own `useFrame`.
10
+ *
11
+ * @capability entity-render-cues live velocity/bob/fire/reload/hit/death cues for a custom renderEntity or viewmodel component
12
+ */
13
+ export declare function useEntityRenderCues(instanceId: string | undefined, tuning?: RenderCueTuning): MutableRefObject<EntityRenderCues>;
@@ -0,0 +1,62 @@
1
+ import { useFrame } from "@react-three/fiber";
2
+ import { useEffect, useRef } from "react";
3
+ import { advanceMotionCues, applyRenderAnimationEvent, applyRenderDeathEvent, applyRenderHitEvent, DEFAULT_FIRE_PULSE_SECONDS, DEFAULT_HIT_PULSE_SECONDS, DEFAULT_RENDER_CUES, } from "@jgengine/core/combat/renderCues";
4
+ import { groundSpeed } from "@jgengine/core/scene/entityStore";
5
+ import { useGameContext } from "@jgengine/react/provider";
6
+ /**
7
+ * Live motion + animation cues for one entity, read from a mutable ref inside
8
+ * your own `useFrame` — no re-render per frame, no diffing the parent group's
9
+ * position, no game-side module map for attack/hit timing. Backs both custom
10
+ * `renderEntity` rigs and a custom first-person viewmodel (#542): call it with
11
+ * `entity.id` / the local player's userId and drive bob/recoil/reload poses
12
+ * from `cuesRef.current` inside the calling component's own `useFrame`.
13
+ *
14
+ * @capability entity-render-cues live velocity/bob/fire/reload/hit/death cues for a custom renderEntity or viewmodel component
15
+ */
16
+ export function useEntityRenderCues(instanceId, tuning) {
17
+ const ctx = useGameContext();
18
+ const cuesRef = useRef({ ...DEFAULT_RENDER_CUES });
19
+ const timersRef = useRef({ fire: 0, hit: 0 });
20
+ useEffect(() => {
21
+ cuesRef.current = { ...DEFAULT_RENDER_CUES };
22
+ timersRef.current = { fire: 0, hit: 0 };
23
+ if (instanceId === undefined)
24
+ return;
25
+ const offAnimation = ctx.game.events.on("entity.animation", (event) => {
26
+ if (event.instanceId !== instanceId)
27
+ return;
28
+ cuesRef.current = applyRenderAnimationEvent(cuesRef.current, event.event);
29
+ if (event.event === "fire")
30
+ timersRef.current.fire = tuning?.firePulseSeconds ?? DEFAULT_FIRE_PULSE_SECONDS;
31
+ });
32
+ const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
33
+ if (event.instanceId !== instanceId)
34
+ return;
35
+ cuesRef.current = applyRenderHitEvent(cuesRef.current);
36
+ timersRef.current.hit = tuning?.hitPulseSeconds ?? DEFAULT_HIT_PULSE_SECONDS;
37
+ });
38
+ const offDied = ctx.game.events.on("entity.died", (event) => {
39
+ if (event.instanceId !== instanceId)
40
+ return;
41
+ cuesRef.current = applyRenderDeathEvent(cuesRef.current);
42
+ });
43
+ return () => {
44
+ offAnimation();
45
+ offHit();
46
+ offDied();
47
+ };
48
+ }, [ctx, instanceId, tuning?.firePulseSeconds, tuning?.hitPulseSeconds]);
49
+ useFrame((_state, delta) => {
50
+ if (instanceId === undefined || delta <= 0)
51
+ return;
52
+ const entity = ctx.scene.entity.get(instanceId);
53
+ if (entity === null)
54
+ return;
55
+ const timers = timersRef.current;
56
+ timers.fire = Math.max(0, timers.fire - delta);
57
+ timers.hit = Math.max(0, timers.hit - delta);
58
+ const advanced = advanceMotionCues(cuesRef.current, groundSpeed(entity), delta, tuning);
59
+ cuesRef.current = { ...advanced, firing: timers.fire > 0, hit: timers.hit > 0 };
60
+ });
61
+ return cuesRef;
62
+ }
@@ -0,0 +1,24 @@
1
+ import type { ModelConfig } from "@jgengine/core/game/playableGame";
2
+ import { type ScatterInstance } from "@jgengine/core/world/scatterRegion";
3
+ /** Props for {@link InstancedScatter}: the resolved instances plus chunking/instance-cap knobs. */
4
+ export interface InstancedScatterProps {
5
+ instances: readonly ScatterInstance[];
6
+ /** Chunk edge in meters; dense fields split into independently frustum-culled buckets. Default 24. */
7
+ chunkSize?: number;
8
+ /** Hard cap on rendered instances. Default 20000. */
9
+ maxInstances?: number;
10
+ /**
11
+ * Per-species override: return a real catalog `ModelConfig` for an item to GPU-instance the actual
12
+ * GLB instead of the stylized proxy; return `null` (or omit the prop) to keep `buildScatterProxy`.
13
+ */
14
+ resolveItem?: (item: string) => ModelConfig | null;
15
+ }
16
+ /**
17
+ * Renders resolved {@link ScatterInstance}s grouped into per-chunk, per-species GPU-instanced draws
18
+ * that frustum-cull independently. Species with no `resolveItem` mapping (or a `null` result) render
19
+ * as stylized proxy models (trunked trees, stacked pines, round bushes, faceted rocks, grass tufts);
20
+ * species `resolveItem` resolves to a `ModelConfig` GPU-instance the real catalog GLB instead. The one
21
+ * runtime foliage renderer shared by the editor's scatter preview and games that consume
22
+ * `resolveScatter` — never one node per placement.
23
+ */
24
+ export declare function InstancedScatter({ instances, chunkSize, maxInstances, resolveItem, }: InstancedScatterProps): import("react").JSX.Element | null;
@@ -0,0 +1,135 @@
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 { beginFallbackSeam, reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
9
+ import { buildScatterProxy } from "./scatterProxies.js";
10
+ import { buildScatterModelSources, disposeScatterModelSources } from "./scatterModels.js";
11
+ /** Chunk edge (m): dense fields split into this-sized buckets, each frustum-culled independently. */
12
+ const DEFAULT_CHUNK_SIZE = 24;
13
+ const DEFAULT_MAX_INSTANCES = 20000;
14
+ /** Groups resolved instances by spatial chunk then species so each batch is one cullable draw. */
15
+ function batchInstances(instances, chunkSize) {
16
+ const batches = [];
17
+ const matrix = new THREE.Matrix4();
18
+ const position = new THREE.Vector3();
19
+ const quaternion = new THREE.Quaternion();
20
+ const euler = new THREE.Euler();
21
+ const scale = new THREE.Vector3();
22
+ for (const chunk of chunkScatterInstances(instances, chunkSize)) {
23
+ const byItem = new Map();
24
+ for (const instance of chunk.instances) {
25
+ position.set(instance.x, instance.y, instance.z);
26
+ euler.set(0, instance.rotationY, 0);
27
+ quaternion.setFromEuler(euler);
28
+ scale.set(instance.scale, instance.scale, instance.scale);
29
+ matrix.compose(position, quaternion, scale);
30
+ const bucket = byItem.get(instance.item);
31
+ if (bucket === undefined)
32
+ byItem.set(instance.item, [matrix.clone()]);
33
+ else
34
+ bucket.push(matrix.clone());
35
+ }
36
+ for (const [item, matrices] of byItem)
37
+ batches.push({ key: `${chunk.key}:${item}`, item, matrices });
38
+ }
39
+ return batches;
40
+ }
41
+ /** One species-in-a-chunk InstancedMesh; frustum-culled as a unit so offscreen chunks cost nothing. */
42
+ function ScatterBatchMesh({ batch, geometry }) {
43
+ const meshRef = useRef(null);
44
+ useEffect(() => {
45
+ const mesh = meshRef.current;
46
+ if (mesh === null)
47
+ return;
48
+ for (let i = 0; i < batch.matrices.length; i += 1)
49
+ mesh.setMatrixAt(i, batch.matrices[i]);
50
+ mesh.count = batch.matrices.length;
51
+ mesh.instanceMatrix.needsUpdate = true;
52
+ mesh.computeBoundingSphere();
53
+ }, [batch]);
54
+ 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 }) }));
55
+ }
56
+ /** One GLB-backed draw source instanced across every chunk of one species. */
57
+ function ScatterModelSourceInstances({ source, matrices }) {
58
+ const meshRef = useRef(null);
59
+ useEffect(() => {
60
+ const mesh = meshRef.current;
61
+ if (mesh === null)
62
+ return;
63
+ const composed = new THREE.Matrix4();
64
+ for (let i = 0; i < matrices.length; i += 1) {
65
+ composed.multiplyMatrices(matrices[i], source.localMatrix);
66
+ mesh.setMatrixAt(i, composed);
67
+ }
68
+ mesh.count = matrices.length;
69
+ mesh.instanceMatrix.needsUpdate = true;
70
+ mesh.computeBoundingSphere();
71
+ }, [matrices, source]);
72
+ return _jsx("instancedMesh", { ref: meshRef, args: [source.geometry, source.material, matrices.length], castShadow: true, receiveShadow: true });
73
+ }
74
+ /** Loads one species' real catalog GLB once, harvests its instanceable draw sources, then GPU-instances them across every chunk that species appears in. */
75
+ function ScatterModelSpecies({ model, batches }) {
76
+ const gltf = useLoader(GLTFLoader, model.url, (loader) => {
77
+ loader.setMeshoptDecoder(MeshoptDecoder);
78
+ });
79
+ const { sources, root } = useMemo(() => buildScatterModelSources(gltf.scene, model), [gltf, model]);
80
+ useEffect(() => () => disposeScatterModelSources(root), [root]);
81
+ return (_jsx(_Fragment, { children: sources.map((source, sourceIndex) => batches.map((batch) => (_jsx(ScatterModelSourceInstances, { source: source, matrices: batch.matrices }, `${batch.key}:${sourceIndex}`)))) }));
82
+ }
83
+ /**
84
+ * Renders resolved {@link ScatterInstance}s grouped into per-chunk, per-species GPU-instanced draws
85
+ * that frustum-cull independently. Species with no `resolveItem` mapping (or a `null` result) render
86
+ * as stylized proxy models (trunked trees, stacked pines, round bushes, faceted rocks, grass tufts);
87
+ * species `resolveItem` resolves to a `ModelConfig` GPU-instance the real catalog GLB instead. The one
88
+ * runtime foliage renderer shared by the editor's scatter preview and games that consume
89
+ * `resolveScatter` — never one node per placement.
90
+ */
91
+ export function InstancedScatter({ instances, chunkSize = DEFAULT_CHUNK_SIZE, maxInstances = DEFAULT_MAX_INSTANCES, resolveItem, }) {
92
+ const batches = useMemo(() => batchInstances(instances.length > maxInstances ? instances.slice(0, maxInstances) : instances, chunkSize), [instances, chunkSize, maxInstances]);
93
+ const { proxyBatches, modelSpecies } = useMemo(() => {
94
+ // Dev diagnostic (no-op unless armed): re-zero the scatter seam before re-tallying which species
95
+ // fall to a stylized proxy (no resolveItem mapping / a null result). Reported once per species.
96
+ beginFallbackSeam("scatter");
97
+ const proxy = [];
98
+ const bySpecies = new Map();
99
+ const knownProxy = new Set();
100
+ for (const batch of batches) {
101
+ const species = bySpecies.get(batch.item);
102
+ if (species !== undefined) {
103
+ species.batches.push(batch);
104
+ continue;
105
+ }
106
+ if (knownProxy.has(batch.item)) {
107
+ proxy.push(batch);
108
+ continue;
109
+ }
110
+ const resolved = resolveItem?.(batch.item) ?? null;
111
+ if (resolved === null) {
112
+ knownProxy.add(batch.item);
113
+ reportFallbackSeam("scatter", "omittedMapping");
114
+ proxy.push(batch);
115
+ continue;
116
+ }
117
+ bySpecies.set(batch.item, { model: resolved, batches: [batch] });
118
+ }
119
+ return { proxyBatches: proxy, modelSpecies: [...bySpecies.entries()] };
120
+ }, [batches, resolveItem]);
121
+ const geometries = useMemo(() => {
122
+ const items = new Set(proxyBatches.map((batch) => batch.item));
123
+ const map = new Map();
124
+ for (const item of items)
125
+ map.set(item, buildScatterProxy(item));
126
+ return map;
127
+ }, [proxyBatches]);
128
+ useEffect(() => () => geometries.forEach((geometry) => geometry.dispose()), [geometries]);
129
+ if (batches.length === 0)
130
+ return null;
131
+ return (_jsxs(_Fragment, { children: [proxyBatches.map((batch) => {
132
+ const geometry = geometries.get(batch.item);
133
+ return geometry === undefined ? null : _jsx(ScatterBatchMesh, { batch: batch, geometry: geometry }, batch.key);
134
+ }), modelSpecies.map(([item, species]) => (_jsx(Suspense, { fallback: null, children: _jsx(ScatterModelSpecies, { model: species.model, batches: species.batches }) }, item)))] }));
135
+ }
@@ -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;