@jgengine/shell 0.18.1 → 0.21.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 (261) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +132 -17
  5. package/dist/Shell3dPresentation.d.ts +5 -11
  6. package/dist/Shell3dPresentation.js +73 -11
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +20 -2
  10. package/dist/ShellHudPresentation.js +41 -6
  11. package/dist/audio/AudioComponents.js +6 -2
  12. package/dist/audio/audioEngine.d.ts +24 -0
  13. package/dist/audio/audioEngine.js +231 -83
  14. package/dist/audio/audioWire.d.ts +3 -0
  15. package/dist/audio/audioWire.js +9 -3
  16. package/dist/audio/loopParams.d.ts +39 -0
  17. package/dist/audio/loopParams.js +43 -0
  18. package/dist/audio/synthEngine.d.ts +7 -1
  19. package/dist/audio/synthEngine.js +103 -38
  20. package/dist/camera/GameCameraRig.js +3 -2
  21. package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
  22. package/dist/camera/GameFirstPersonCamera.js +68 -27
  23. package/dist/camera/Viewports.d.ts +34 -0
  24. package/dist/camera/Viewports.js +123 -0
  25. package/dist/camera/cameraRigs.js +76 -62
  26. package/dist/camera/chaseHeading.d.ts +9 -0
  27. package/dist/camera/chaseHeading.js +10 -0
  28. package/dist/camera/customMuzzle.d.ts +14 -0
  29. package/dist/camera/customMuzzle.js +68 -0
  30. package/dist/camera/index.d.ts +1 -0
  31. package/dist/camera/index.js +1 -0
  32. package/dist/camera/orbitCameraMath.js +2 -2
  33. package/dist/camera/rigMath.d.ts +82 -4
  34. package/dist/camera/rigMath.js +133 -15
  35. package/dist/camera/rigResolve.d.ts +3 -0
  36. package/dist/camera/rigResolve.js +11 -0
  37. package/dist/combat/ProjectileModels.d.ts +31 -0
  38. package/dist/combat/ProjectileModels.js +70 -0
  39. package/dist/commandSink.d.ts +1 -2
  40. package/dist/commandSink.js +3 -2
  41. package/dist/defineGame.d.ts +19 -3
  42. package/dist/defineGame.js +39 -7
  43. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  44. package/dist/devtools/DevtoolsOverlay.js +15 -167
  45. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  46. package/dist/devtools/DevtoolsRuntime.js +61 -0
  47. package/dist/devtools/agentBridge.d.ts +6 -0
  48. package/dist/devtools/agentBridge.js +16 -2
  49. package/dist/devtools/devtoolsReports.d.ts +68 -0
  50. package/dist/devtools/devtoolsReports.js +100 -0
  51. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  52. package/dist/devtools/latencyInstrumentation.js +11 -0
  53. package/dist/devtools/playtestProbe.d.ts +4 -0
  54. package/dist/devtools/playtestProbe.js +22 -0
  55. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  56. package/dist/diagnostics/FpsCounter.js +32 -0
  57. package/dist/drivers/FrameDriver.js +35 -20
  58. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  59. package/dist/drivers/FrameRateLimiter.js +39 -0
  60. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  61. package/dist/drivers/HudOnlyDriver.js +32 -14
  62. package/dist/environment/DayNightSky.js +5 -5
  63. package/dist/environment/Daylight.d.ts +35 -1
  64. package/dist/environment/Daylight.js +98 -28
  65. package/dist/environment/EnvironmentScene.js +11 -10
  66. package/dist/environment/VolumetricClouds.js +20 -2
  67. package/dist/environment/daylightCycle.d.ts +20 -0
  68. package/dist/environment/daylightCycle.js +59 -7
  69. package/dist/environment/sunShadowMath.d.ts +45 -0
  70. package/dist/environment/sunShadowMath.js +50 -0
  71. package/dist/input/gamepadPoll.d.ts +47 -0
  72. package/dist/input/gamepadPoll.js +142 -0
  73. package/dist/input/gamepadSource.d.ts +11 -1
  74. package/dist/input/gamepadSource.js +117 -71
  75. package/dist/input/mouseLook.js +2 -1
  76. package/dist/input/padHaptics.d.ts +17 -0
  77. package/dist/input/padHaptics.js +32 -0
  78. package/dist/input/pointerLock.d.ts +11 -0
  79. package/dist/input/pointerLock.js +19 -0
  80. package/dist/inputSink.d.ts +6 -6
  81. package/dist/inputSink.js +60 -21
  82. package/dist/lazyShell3dPresentation.d.ts +9 -0
  83. package/dist/lazyShell3dPresentation.js +24 -0
  84. package/dist/materialOverride.d.ts +42 -0
  85. package/dist/materialOverride.js +95 -36
  86. package/dist/multiplayer.d.ts +33 -3
  87. package/dist/multiplayer.js +105 -22
  88. package/dist/pointer/pointerService.d.ts +2 -0
  89. package/dist/pointer/pointerService.js +12 -2
  90. package/dist/postfx/PostProcessing.d.ts +11 -1
  91. package/dist/postfx/PostProcessing.js +14 -42
  92. package/dist/postfx/gradeShader.d.ts +1 -1
  93. package/dist/postfx/gradeShader.js +30 -2
  94. package/dist/postfx/shadowPrepass.d.ts +3 -0
  95. package/dist/postfx/shadowPrepass.js +18 -0
  96. package/dist/postfx/stylizePass.d.ts +23 -0
  97. package/dist/postfx/stylizePass.js +216 -0
  98. package/dist/presentationRecovery.d.ts +75 -0
  99. package/dist/presentationRecovery.js +204 -0
  100. package/dist/presentationRecovery3d.d.ts +4 -0
  101. package/dist/presentationRecovery3d.js +32 -0
  102. package/dist/presentationResolve.d.ts +8 -1
  103. package/dist/presentationResolve.js +5 -2
  104. package/dist/render/CascadedShadows.d.ts +7 -2
  105. package/dist/render/CascadedShadows.js +98 -20
  106. package/dist/render/EnvironmentLighting.js +74 -35
  107. package/dist/render/HairCards.d.ts +7 -0
  108. package/dist/render/HairCards.js +23 -0
  109. package/dist/render/SceneModels.js +52 -78
  110. package/dist/render/SkinnedInstances.d.ts +91 -0
  111. package/dist/render/SkinnedInstances.js +249 -0
  112. package/dist/render/TextureFiltering.d.ts +16 -0
  113. package/dist/render/TextureFiltering.js +64 -0
  114. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  115. package/dist/render/authoredSurfaceMaterial.js +108 -0
  116. package/dist/render/displayColor.d.ts +33 -0
  117. package/dist/render/displayColor.js +138 -0
  118. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  119. package/dist/render/materialAppearanceSignals.js +58 -0
  120. package/dist/render/materialAsset.d.ts +98 -0
  121. package/dist/render/materialAsset.js +454 -0
  122. package/dist/render/measureBounds.js +16 -3
  123. package/dist/render/measureCollisionMesh.js +8 -1
  124. package/dist/render/modelAssets.d.ts +8 -0
  125. package/dist/render/modelAssets.js +43 -0
  126. package/dist/render/modelBindPose.d.ts +5 -0
  127. package/dist/render/modelBindPose.js +35 -0
  128. package/dist/render/modelLoad.d.ts +32 -0
  129. package/dist/render/modelLoad.js +70 -1
  130. package/dist/render/modelNodeVisibility.d.ts +6 -0
  131. package/dist/render/modelNodeVisibility.js +18 -0
  132. package/dist/render/modelRender.d.ts +18 -1
  133. package/dist/render/modelRender.js +81 -3
  134. package/dist/render/projectedOverdraw.d.ts +35 -0
  135. package/dist/render/projectedOverdraw.js +204 -0
  136. package/dist/render/resolveModel.d.ts +12 -2
  137. package/dist/render/resolveModel.js +25 -4
  138. package/dist/render/rigNode.d.ts +26 -0
  139. package/dist/render/rigNode.js +32 -0
  140. package/dist/render/spriteRender.d.ts +4 -0
  141. package/dist/render/spriteRender.js +5 -0
  142. package/dist/render/staticShapeInstances.d.ts +34 -0
  143. package/dist/render/staticShapeInstances.js +78 -0
  144. package/dist/render/useFootIk.d.ts +53 -4
  145. package/dist/render/useFootIk.js +262 -70
  146. package/dist/render/useModelAnimation.d.ts +50 -0
  147. package/dist/render/useModelAnimation.js +245 -99
  148. package/dist/render/useModelInstance.d.ts +25 -0
  149. package/dist/render/useModelInstance.js +43 -0
  150. package/dist/scatter/scatterModels.d.ts +3 -2
  151. package/dist/scatter/scatterModels.js +15 -7
  152. package/dist/scene/AuthoredScene.d.ts +23 -6
  153. package/dist/scene/AuthoredScene.js +46 -9
  154. package/dist/scene/CityRenderer.js +6 -6
  155. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  156. package/dist/settings/QuickControls.js +1 -1
  157. package/dist/settings/SettingsMenu.js +11 -18
  158. package/dist/settings/SettingsRuntime.d.ts +5 -0
  159. package/dist/settings/SettingsRuntime.js +29 -2
  160. package/dist/settings/appliedSettings.d.ts +9 -3
  161. package/dist/settings/appliedSettings.js +6 -5
  162. package/dist/settings/settingsController.d.ts +3 -0
  163. package/dist/settings/settingsController.js +47 -3
  164. package/dist/shellControlSuspension.d.ts +18 -0
  165. package/dist/shellControlSuspension.js +28 -0
  166. package/dist/shellInputPublication.d.ts +28 -0
  167. package/dist/shellInputPublication.js +49 -0
  168. package/dist/shellPointerInput.d.ts +18 -0
  169. package/dist/shellPointerInput.js +49 -0
  170. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  171. package/dist/structures/BuildingKitBatch.js +35 -56
  172. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  173. package/dist/structures/BuildingRenderBudget.js +54 -0
  174. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  175. package/dist/structures/GeneratedBuilding.js +33 -22
  176. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  177. package/dist/structures/buildingKitMaterials.js +70 -0
  178. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  179. package/dist/structures/buildingSpatialBatch.js +45 -0
  180. package/dist/structures/buildingSurface.d.ts +21 -0
  181. package/dist/structures/buildingSurface.js +134 -0
  182. package/dist/terrain/GrassField.d.ts +2 -1
  183. package/dist/terrain/GrassField.js +59 -5
  184. package/dist/terrain/grassGeometry.d.ts +10 -0
  185. package/dist/terrain/grassGeometry.js +55 -0
  186. package/dist/terrain/grassMaterial.js +2 -2
  187. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  188. package/dist/terrain/terrainDetailMaterial.js +7 -7
  189. package/dist/touch/TouchControlsOverlay.js +4 -4
  190. package/dist/useShellMultiplayerSync.d.ts +1 -1
  191. package/dist/useShellMultiplayerSync.js +9 -3
  192. package/dist/vfx/ParticleField.d.ts +23 -20
  193. package/dist/vfx/ParticleField.js +168 -50
  194. package/dist/visibility/CullingProvider.js +2 -62
  195. package/dist/visibility/cullingDriver.d.ts +9 -0
  196. package/dist/visibility/cullingDriver.js +68 -0
  197. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  198. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  199. package/dist/weather/DustField.d.ts +2 -1
  200. package/dist/weather/DustField.js +17 -3
  201. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  202. package/dist/weather/FireSpreadLayer.js +5 -11
  203. package/dist/weather/LightningStrike.d.ts +4 -1
  204. package/dist/weather/LightningStrike.js +13 -17
  205. package/dist/weather/RainField.d.ts +2 -1
  206. package/dist/weather/RainField.js +17 -3
  207. package/dist/weather/RainImpactField.d.ts +16 -0
  208. package/dist/weather/RainImpactField.js +89 -0
  209. package/dist/weather/SnowField.d.ts +2 -1
  210. package/dist/weather/SnowField.js +17 -3
  211. package/dist/weather/WeatherLayer.d.ts +11 -2
  212. package/dist/weather/WeatherLayer.js +30 -6
  213. package/dist/weather/index.d.ts +2 -0
  214. package/dist/weather/index.js +2 -0
  215. package/dist/weather/weatherMath.d.ts +2 -0
  216. package/dist/weather/weatherMath.js +8 -0
  217. package/dist/weather/weatherShelter.d.ts +18 -0
  218. package/dist/weather/weatherShelter.js +75 -0
  219. package/dist/weather/weatherUniforms.d.ts +40 -1
  220. package/dist/weather/weatherUniforms.js +29 -9
  221. package/dist/world/AuthoredFlocks.d.ts +22 -0
  222. package/dist/world/AuthoredFlocks.js +87 -0
  223. package/dist/world/StaticObjectModels.d.ts +8 -0
  224. package/dist/world/StaticObjectModels.js +75 -0
  225. package/dist/world/WorldHud.d.ts +14 -6
  226. package/dist/world/WorldHud.js +25 -12
  227. package/dist/world/WorldParticles.d.ts +10 -23
  228. package/dist/world/WorldParticles.js +210 -62
  229. package/dist/world/WorldScene.d.ts +3 -1
  230. package/dist/world/WorldScene.js +41 -26
  231. package/dist/world/staticObjectBatches.d.ts +35 -0
  232. package/dist/world/staticObjectBatches.js +163 -0
  233. package/dist/world/worldBarSamples.d.ts +37 -33
  234. package/dist/world/worldBarSamples.js +117 -75
  235. package/package.json +4 -4
  236. package/skills/ce-handoff/SKILL.md +2 -2
  237. package/skills/game-design/SKILL.md +2 -2
  238. package/skills/jgengine/SKILL.md +28 -6
  239. package/skills/jgengine/api.md +139 -99
  240. package/skills/jgengine/capabilities.md +28 -0
  241. package/skills/jgengine/recipes/minimal-game.md +10 -2
  242. package/skills/jgengine-ui/SKILL.md +16 -17
  243. package/skills/jgengine-ui/api.md +603 -365
  244. package/skills/jgengine-ui/capabilities.md +131 -1
  245. package/skills/jgengine-ui/reference-primitives.md +179 -0
  246. package/skills/jgengine-ui/reference.md +4 -81
  247. package/skills/jgengine-world/SKILL.md +25 -4
  248. package/skills/jgengine-world/api.md +1045 -655
  249. package/skills/jgengine-world/capabilities.md +246 -9
  250. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  251. package/skills/jgengine-world/recipes/controls.md +48 -0
  252. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  253. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  254. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  255. package/skills/jgengine-world/reference.md +180 -4
  256. package/skills/jgengine-world/visibility.md +139 -0
  257. package/skills/level-design/SKILL.md +1 -1
  258. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
  259. package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
  260. package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
  261. package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
@@ -1,3 +1,4 @@
1
+ import { warnOnce } from "@jgengine/core/devtools/warnOnce";
1
2
  /**
2
3
  * The `ModelConfig` a resolved catalog ref renders as. Stamps `animation: "auto"` so any rigged
3
4
  * catalog asset animates by default — the shell derives idle/walk/run states and one-shots from
@@ -7,6 +8,7 @@
7
8
  function catalogModelConfig(ref) {
8
9
  return {
9
10
  url: ref.url,
11
+ ...(ref.anchor === undefined ? {} : { anchor: ref.anchor }),
10
12
  ...(ref.dims === undefined ? {} : { dims: ref.dims }),
11
13
  ...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
12
14
  animation: "auto",
@@ -39,6 +41,18 @@ export function tryResolveCatalogModel(id, assets) {
39
41
  return undefined;
40
42
  return catalogModelConfig(ref);
41
43
  }
44
+ /** Resolve a live object's catalog and animation, retaining unchanged base model identities. @internal */
45
+ export function resolveObjectModel(object, models, assets, cache) {
46
+ const input = models?.[object.catalogId];
47
+ const ref = assets.resolve(typeof input === "string" ? input : object.catalogId);
48
+ const cached = cache.get(object.catalogId);
49
+ const resolved = cached !== undefined && cached.input === input && cached.ref === ref ? cached.model :
50
+ resolveModel(input, assets, { seam: "objectModels", key: object.catalogId }) ?? tryResolveCatalogModel(object.catalogId, assets);
51
+ if (cached === undefined || cached.model !== resolved || cached.input !== input || cached.ref !== ref) {
52
+ cache.set(object.catalogId, { input, ref, model: resolved });
53
+ }
54
+ return resolved !== undefined && object.animation !== undefined ? { ...resolved, animation: object.animation } : resolved;
55
+ }
42
56
  /**
43
57
  * Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
44
58
  * so mounts receive fully-resolved `ModelConfig`s.
@@ -74,6 +88,9 @@ export function resolveEntityModel(value, assets, key) {
74
88
  * @internal
75
89
  */
76
90
  export function pickModel(assets, pick) {
91
+ if (pick.fallbackModel !== undefined && !assets.has(pick.fallbackModel)) {
92
+ warnOnce(`pickModel:fallback:${pick.fallbackModel}`, `[jgengine] fallbackModel "${pick.fallbackModel}" is not in the asset catalog, so it can never stand in for "${pick.model ?? "(none)"}" — fix the id or declare its pack in the catalog sources.`);
93
+ }
77
94
  for (const id of [pick.model, pick.fallbackModel]) {
78
95
  if (id === undefined)
79
96
  continue;
@@ -110,17 +127,21 @@ function softModelLookup(map, assets) {
110
127
  * The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
111
128
  * object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
112
129
  * ids (a missing catalog id keeps the default collider instead of throwing — the render path already
113
- * reports it), passes inline configs through. `undefined` when the playable maps no models at all.
130
+ * reports it), passes inline configs through. Unmapped objects resolve their own catalog id,
131
+ * matching WorldScene; `undefined` when both the catalog and model maps are empty.
114
132
  * @internal
115
133
  */
116
134
  export function contextModels(playable) {
117
135
  const entity = softModelLookup(playable.entityModels, playable.game.assets);
118
- const object = softModelLookup(playable.objectModels, playable.game.assets);
119
- if (entity === undefined && object === undefined)
136
+ const explicitObject = softModelLookup(playable.objectModels, playable.game.assets);
137
+ if (entity === undefined && explicitObject === undefined && playable.game.assets.ids().length === 0)
120
138
  return undefined;
139
+ const object = (key) => playable.objectModels?.[key] === undefined
140
+ ? tryResolveCatalogModel(key, playable.game.assets)
141
+ : explicitObject?.(key);
121
142
  return {
122
143
  ...(entity === undefined ? {} : { entity }),
123
- ...(object === undefined ? {} : { object }),
144
+ object,
124
145
  };
125
146
  }
126
147
  /**
@@ -0,0 +1,26 @@
1
+ import type { Object3D } from "three";
2
+ /** A missing or nonunique node reference that a rig author can repair. */
3
+ export interface RigNodeDiagnostic {
4
+ code: "missing-node" | "ambiguous-node";
5
+ message: string;
6
+ }
7
+ /** A node on this model instance, or a diagnostic instead of an arbitrary attachment target. */
8
+ export type RigNodeResolution = {
9
+ node: Object3D;
10
+ matchedBy: "runtime" | "authored";
11
+ diagnostic?: undefined;
12
+ } | {
13
+ node: undefined;
14
+ matchedBy?: undefined;
15
+ diagnostic: RigNodeDiagnostic;
16
+ };
17
+ /**
18
+ * Resolves a bone or attachment slot by its exact runtime name first, then by the original
19
+ * imported name preserved as `userData.name`. GLTFLoader sanitizes names such as `handslot.r`
20
+ * to `handslotr`; authored references remain usable without renaming the animated hierarchy.
21
+ * Both lookups require a unique target. Call on model load or slot changes, not each frame,
22
+ * and keep the result with that model instance rather than reusing a cached asset's node.
23
+ *
24
+ * @capability imported-rig-node resolve original imported bone and slot names with ambiguity diagnostics
25
+ */
26
+ export declare function resolveRigNode(rig: Object3D, name: string): RigNodeResolution;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves a bone or attachment slot by its exact runtime name first, then by the original
3
+ * imported name preserved as `userData.name`. GLTFLoader sanitizes names such as `handslot.r`
4
+ * to `handslotr`; authored references remain usable without renaming the animated hierarchy.
5
+ * Both lookups require a unique target. Call on model load or slot changes, not each frame,
6
+ * and keep the result with that model instance rather than reusing a cached asset's node.
7
+ *
8
+ * @capability imported-rig-node resolve original imported bone and slot names with ambiguity diagnostics
9
+ */
10
+ export function resolveRigNode(rig, name) {
11
+ const exact = [];
12
+ const authored = [];
13
+ rig.traverse((node) => {
14
+ if (node.name === name)
15
+ exact.push(node);
16
+ if (node.userData.name === name)
17
+ authored.push(node);
18
+ });
19
+ const matches = exact.length > 0 ? exact : authored;
20
+ const matchedBy = exact.length > 0 ? "runtime" : "authored";
21
+ if (matches.length === 1)
22
+ return { node: matches[0], matchedBy };
23
+ if (matches.length > 1)
24
+ return {
25
+ node: undefined,
26
+ diagnostic: { code: "ambiguous-node", message: `bone/slot "${name}" matches ${matches.length} ${matchedBy} names on this rig. Choose a unique runtime node name or repair duplicate imported names; no node was selected.` },
27
+ };
28
+ return {
29
+ node: undefined,
30
+ diagnostic: { code: "missing-node", message: `bone/slot "${name}" was not found by runtime or original imported name on this rig. Check the model's node names and configure a supported slot.` },
31
+ };
32
+ }
@@ -0,0 +1,4 @@
1
+ import type * as THREE from "three";
2
+ import type { SpriteAtlas } from "@jgengine/core/assets/spriteAtlas";
3
+ /** Update atlas UVs without invalidating the shared image upload. @internal */
4
+ export declare function syncSpriteFrame(texture: THREE.Texture, frame: SpriteAtlas["frames"][string], size: readonly [number, number]): void;
@@ -0,0 +1,5 @@
1
+ /** Update atlas UVs without invalidating the shared image upload. @internal */
2
+ export function syncSpriteFrame(texture, frame, size) {
3
+ texture.repeat.set(frame.w / size[0], frame.h / size[1]);
4
+ texture.offset.set(frame.x / size[0], 1 - (frame.y + frame.h) / size[1]);
5
+ }
@@ -0,0 +1,34 @@
1
+ import * as THREE from "three";
2
+ import type { ModelShadowMode } from "./modelRender.js";
3
+ import { type AuthoredSurfaceConfig, type SurfaceShape } from "./authoredSurfaceMaterial.js";
4
+ /** One editor-derived static primitive. A box spans one unit; a cylinder has unit radius and height before scale. */
5
+ export interface StaticShapeInstance {
6
+ shape: SurfaceShape;
7
+ position: readonly [number, number, number];
8
+ rotation?: readonly [number, number, number];
9
+ scale: readonly [number, number, number];
10
+ surface: AuthoredSurfaceConfig;
11
+ shadows?: ModelShadowMode;
12
+ /** Cylinder tessellation; default 16. */
13
+ radialSegments?: number;
14
+ }
15
+ /** A compatible static draw group. @internal */
16
+ export interface StaticShapeBatch {
17
+ key: string;
18
+ shape: SurfaceShape;
19
+ radialSegments: number;
20
+ surface: AuthoredSurfaceConfig;
21
+ shadows: ModelShadowMode;
22
+ instances: readonly StaticShapeInstance[];
23
+ }
24
+ /** Plan compatible material/shape/shadow groups without changing authored transforms. @internal */
25
+ export declare function groupStaticShapeInstances(instances: readonly StaticShapeInstance[]): StaticShapeBatch[];
26
+ /** Upload static transforms and refresh conservative aggregate bounds after a prop change. @internal */
27
+ export declare function writeStaticShapeInstances(mesh: THREE.InstancedMesh, instances: readonly StaticShapeInstance[]): void;
28
+ /**
29
+ * Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
30
+ * @capability static-shape-instances render authored primitive transforms as static material-compatible instance batches
31
+ */
32
+ export declare function StaticShapeInstances({ instances }: {
33
+ instances: readonly StaticShapeInstance[];
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, useLayoutEffect, useMemo, useRef } from "react";
3
+ import * as THREE from "three";
4
+ import { AuthoredSurfaceMaterial, authoredSurfaceKey } from "./authoredSurfaceMaterial.js";
5
+ import { ModelFallbackBoundary } from "./SceneModels.js";
6
+ import { useDisposable } from "./useDisposable.js";
7
+ /** Plan compatible material/shape/shadow groups without changing authored transforms. @internal */
8
+ export function groupStaticShapeInstances(instances) {
9
+ const groups = new Map();
10
+ for (const instance of instances) {
11
+ if (![...instance.position, ...instance.scale, ...(instance.rotation ?? [])].every(Number.isFinite) || instance.scale.some(value => value <= 0))
12
+ throw new Error("Static shape transforms must be finite with positive scale");
13
+ if ([instance.surface.displacementScale, instance.surface.displacementBias].some(value => value !== undefined && !Number.isFinite(value)))
14
+ throw new Error("Static shape displacement must be finite");
15
+ const radialSegments = instance.shape === "cylinder" ? instance.radialSegments ?? 16 : 0;
16
+ if (instance.shape === "cylinder" && (!Number.isInteger(radialSegments) || radialSegments < 3))
17
+ throw new Error("Cylinder radialSegments must be an integer >= 3");
18
+ const shadows = instance.shadows ?? "both";
19
+ const key = JSON.stringify([instance.shape, radialSegments, shadows, authoredSurfaceKey(instance.surface)]);
20
+ let group = groups.get(key);
21
+ if (group === undefined) {
22
+ group = { key, shape: instance.shape, radialSegments, surface: instance.surface, shadows, instances: [] };
23
+ groups.set(key, group);
24
+ }
25
+ group.instances.push(instance);
26
+ }
27
+ return [...groups.values()];
28
+ }
29
+ /** Upload static transforms and refresh conservative aggregate bounds after a prop change. @internal */
30
+ export function writeStaticShapeInstances(mesh, instances) {
31
+ const transform = new THREE.Object3D();
32
+ instances.forEach((instance, index) => {
33
+ transform.position.set(...instance.position);
34
+ transform.rotation.set(...(instance.rotation ?? [0, 0, 0]));
35
+ transform.scale.set(...instance.scale);
36
+ transform.updateMatrix();
37
+ mesh.setMatrixAt(index, transform.matrix);
38
+ });
39
+ mesh.count = instances.length;
40
+ if (mesh.geometry.boundingBox === null)
41
+ mesh.geometry.computeBoundingBox();
42
+ const heightSurface = instances[0]?.surface;
43
+ const displacedBounds = mesh.geometry.boundingBox.clone();
44
+ if (heightSurface?.maps?.height !== undefined) {
45
+ const bias = heightSurface.displacementBias ?? 0;
46
+ const scale = heightSurface.displacementScale ?? 1;
47
+ const displacement = Math.max(Math.abs(bias), Math.abs(bias + scale));
48
+ displacedBounds.expandByScalar(displacement + Number.EPSILON * Math.max(1, displacement) * 8);
49
+ }
50
+ mesh.instanceMatrix.needsUpdate = true;
51
+ mesh.boundingBox = new THREE.Box3();
52
+ const matrix = new THREE.Matrix4(), bounds = new THREE.Box3();
53
+ for (let index = 0; index < instances.length; index++) {
54
+ mesh.getMatrixAt(index, matrix);
55
+ mesh.boundingBox.union(bounds.copy(displacedBounds).applyMatrix4(matrix));
56
+ }
57
+ mesh.boundingSphere = mesh.boundingBox.getBoundingSphere(new THREE.Sphere());
58
+ // Three scales spheres by the longest matrix column. sqrt(3) also encloses affine parent shear.
59
+ mesh.boundingSphere.radius *= Math.sqrt(3);
60
+ }
61
+ function ShapeBatch({ batch }) {
62
+ const ref = useRef(null);
63
+ const geometry = useDisposable(() => batch.shape === "box" ? new THREE.BoxGeometry(1, 1, 1) : new THREE.CylinderGeometry(1, 1, 1, batch.radialSegments), [batch.shape, batch.radialSegments]);
64
+ useLayoutEffect(() => {
65
+ if (ref.current !== null)
66
+ writeStaticShapeInstances(ref.current, batch.instances);
67
+ }, [batch.instances]);
68
+ const fallback = _jsx(AuthoredSurfaceMaterial, { surface: { ...batch.surface, maps: undefined }, shape: batch.shape });
69
+ return _jsx("instancedMesh", { ref: ref, args: [geometry, undefined, batch.instances.length], castShadow: batch.shadows === "both" || batch.shadows === "cast", receiveShadow: batch.shadows === "both" || batch.shadows === "receive", children: _jsx(ModelFallbackBoundary, { fallback: fallback, url: Object.values(batch.surface.maps ?? {}).join(","), seam: "object", children: _jsx(Suspense, { fallback: fallback, children: _jsx(AuthoredSurfaceMaterial, { surface: batch.surface, shape: batch.shape }) }) }) });
70
+ }
71
+ /**
72
+ * Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
73
+ * @capability static-shape-instances render authored primitive transforms as static material-compatible instance batches
74
+ */
75
+ export function StaticShapeInstances({ instances }) {
76
+ const batches = useMemo(() => groupStaticShapeInstances(instances), [instances]);
77
+ return _jsx(_Fragment, { children: batches.map(batch => _jsx(ShapeBatch, { batch: batch }, batch.key)) });
78
+ }
@@ -1,9 +1,58 @@
1
1
  import * as THREE from "three";
2
2
  import type { GameContext } from "@jgengine/core/runtime/gameContextTypes";
3
3
  import type { ModelConfig } from "@jgengine/core/game/playableGame";
4
+ import type { SceneRaycastHit, SceneRaycastInput } from "@jgengine/core/scene/sceneRaycast";
4
5
  type FootIkConfig = NonNullable<ModelConfig["ik"]>;
5
- /** Applies one frame of foot IK to a loaded rig. Exported for renderer tests and custom model hosts. */
6
- export declare function applyFootIk(scene: THREE.Object3D, config: FootIkConfig, raycast: GameContext["scene"]["raycast"], weight: number, cameraTarget?: readonly [number, number, number]): boolean;
7
- /** Runs foot IK after the model animation mixer, fading the correction out while the entity is airborne. */
8
- export declare function useFootIk(scene: THREE.Object3D, config: FootIkConfig | undefined, ctx: GameContext | null, instanceId?: string): void;
6
+ type GroundProbe = (input: SceneRaycastInput) => SceneRaycastHit | null;
7
+ interface IkLeg {
8
+ root: THREE.Object3D;
9
+ mid: THREE.Object3D;
10
+ tip: THREE.Object3D;
11
+ /** Rest-pose ankle height above the sole divided by rest leg length, so it survives any scale. */
12
+ ankleRatio: number;
13
+ }
14
+ interface PoseRecord {
15
+ bone: THREE.Object3D;
16
+ animated: {
17
+ q: THREE.Quaternion;
18
+ p: THREE.Vector3;
19
+ };
20
+ written: {
21
+ q: THREE.Quaternion;
22
+ p: THREE.Vector3;
23
+ };
24
+ }
25
+ /** Bones resolved once per loaded scene for {@link applyFootIk}. */
26
+ export interface FootIkRig {
27
+ legs: IkLeg[];
28
+ pelvis: THREE.Object3D | null;
29
+ head: THREE.Object3D | null;
30
+ maxAdjust: number;
31
+ alignToGround: number;
32
+ /** Pose before and after last frame's correction, so a bone no clip animates never accumulates it. */
33
+ poses: PoseRecord[];
34
+ }
35
+ /** Per-model state carried between frames: the faded weight and the smoothed pelvis drop. */
36
+ export interface FootIkState {
37
+ weight: number;
38
+ pelvis: number;
39
+ }
40
+ /**
41
+ * Resolves foot-IK bones on a loaded rig: explicit chains, or legs found by bone name for `"auto"`
42
+ * and configs without `feet`. Returns `null` when no leg resolves.
43
+ */
44
+ export declare function resolveFootIkRig(scene: THREE.Object3D, config: FootIkConfig): FootIkRig | null;
45
+ /**
46
+ * Applies one frame of foot IK after the animation mixer: probes the ground under each foot,
47
+ * resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending
48
+ * toward the animated knee, and tilts planted feet to the ground. Returns whether the feet are
49
+ * grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
50
+ */
51
+ export declare function applyFootIk(rig: FootIkRig, originY: number, probe: GroundProbe, state: FootIkState, delta: number, cameraTarget?: readonly [number, number, number]): boolean;
52
+ /**
53
+ * Runs foot IK after the model's animation mixer for `ModelConfig.ik`. Ground probes hit terrain and
54
+ * blocking physical objects, never the model's own entity. `groundOffset` is the model-space height
55
+ * its soles rest on (`ModelConfig.y`).
56
+ */
57
+ export declare function useFootIk(scene: THREE.Object3D, config: FootIkConfig | undefined, ctx: GameContext | null, instanceId?: string, groundOffset?: number): void;
9
58
  export {};