@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,5 +1,6 @@
1
1
  import * as THREE from "three";
2
2
  import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
3
+ import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
3
4
  import { type AssetLoadDiagnosis } from "@jgengine/core/scene/assetDiagnostics";
4
5
  /**
5
6
  * The shared GLB {@link THREE.LoadingManager}'s `onError` sink. It fires for EVERY item the manager
@@ -61,6 +62,35 @@ export declare function modelLoadFallbacks(): readonly {
61
62
  }[];
62
63
  /** Drops the recorded placeholder list. For tests and between capture runs. @internal */
63
64
  export declare function clearModelLoadFallbacks(): void;
65
+ /** Per-model triangle ceiling before the loader warns. A prop is a few thousand triangles; a hero rig tens of thousands. */
66
+ export declare const DEFAULT_MODEL_TRIANGLE_BUDGET = 100000;
67
+ /** Retune the per-model triangle budget (a cinematic game may raise it; a mobile target lowers it). Non-finite or non-positive values restore the default. */
68
+ export declare function setModelTriangleBudget(triangles: number): void;
69
+ /** Triangles a loaded model contributes per draw, summing every mesh (instanced meshes count once per instance). @internal */
70
+ export declare function countSceneTriangles(root: THREE.Object3D): number;
71
+ /** One over-budget model as {@link heavyModels} reports it: the URL the loader fetched, its triangle count, and the budget it exceeded. */
72
+ export interface HeavyModelRecord {
73
+ url: string;
74
+ triangles: number;
75
+ budget: number;
76
+ }
77
+ /**
78
+ * Record a loaded model's triangle count against the budget: over budget warns once per URL and lands in
79
+ * {@link heavyModels}; under budget is silent. Split out so the wiring is unit-testable without a GL context.
80
+ * @internal
81
+ */
82
+ export declare function recordModelTriangles(url: string, triangles: number, overrides?: {
83
+ warn?: (record: HeavyModelRecord) => void;
84
+ }): void;
85
+ /**
86
+ * Every model the shared loader resolved whose triangle count exceeds the budget, in load order. A heavy
87
+ * prop is invisible in a screenshot and only shows up as a slow frame, so the loader measures each GLB as it
88
+ * lands and this exposes the verdict for `debug_snapshot` probes and smoke tests to assert against.
89
+ * @capability heavy-models models over the per-model triangle budget, for perf probes and smoke assertions
90
+ */
91
+ export declare function heavyModels(): readonly HeavyModelRecord[];
92
+ /** Drops the heavy-model list and the once-per-URL warning memory. For tests and between capture runs. @internal */
93
+ export declare function clearHeavyModels(): void;
64
94
  /**
65
95
  * A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
66
96
  * gracefully: when a load errors, it probes the URL and — for a diagnosed broken
@@ -82,6 +112,8 @@ export interface ModelLoaderConfig {
82
112
  export declare function configureModelLoaders(options?: ModelLoaderConfig): Readonly<Required<ModelLoaderConfig>>;
83
113
  /** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
84
114
  export declare function detectKtx2Support(renderer: THREE.WebGLRenderer): void;
115
+ /** Shared transcoder for authored material maps; GPU support is detected by the canvas. @internal */
116
+ export declare function materialKtx2Loader(): KTX2Loader;
85
117
  /**
86
118
  * How long the shared GLB loader has been idle, in ms — `0` while any model is still
87
119
  * in flight. A capture host reads this to wait for streaming to finish instead of
@@ -140,6 +140,63 @@ export function modelLoadFallbacks() {
140
140
  export function clearModelLoadFallbacks() {
141
141
  servedFallbacks.length = 0;
142
142
  }
143
+ /** Per-model triangle ceiling before the loader warns. A prop is a few thousand triangles; a hero rig tens of thousands. */
144
+ export const DEFAULT_MODEL_TRIANGLE_BUDGET = 100_000;
145
+ let modelTriangleBudget = DEFAULT_MODEL_TRIANGLE_BUDGET;
146
+ /** Retune the per-model triangle budget (a cinematic game may raise it; a mobile target lowers it). Non-finite or non-positive values restore the default. */
147
+ export function setModelTriangleBudget(triangles) {
148
+ modelTriangleBudget = Number.isFinite(triangles) && triangles > 0 ? triangles : DEFAULT_MODEL_TRIANGLE_BUDGET;
149
+ }
150
+ /** Triangles a loaded model contributes per draw, summing every mesh (instanced meshes count once per instance). @internal */
151
+ export function countSceneTriangles(root) {
152
+ let total = 0;
153
+ root.traverse((node) => {
154
+ const mesh = node;
155
+ if (mesh.isMesh !== true || mesh.geometry === undefined)
156
+ return;
157
+ const geometry = mesh.geometry;
158
+ const vertices = geometry.index !== null ? geometry.index.count : geometry.attributes.position?.count ?? 0;
159
+ const instances = mesh.isInstancedMesh === true && typeof mesh.count === "number" ? mesh.count : 1;
160
+ total += Math.floor(vertices / 3) * instances;
161
+ });
162
+ return total;
163
+ }
164
+ const heavyModelRecords = [];
165
+ const warnedHeavyUrls = new Set();
166
+ function warnHeavyModel(record) {
167
+ if (typeof console === "undefined")
168
+ return;
169
+ console.warn(`[jgengine] model ${record.url} has ${record.triangles.toLocaleString()} triangles (budget ${record.budget.toLocaleString()}). A game prop should be a few thousand — pick a lighter asset or decimate it.`);
170
+ }
171
+ /**
172
+ * Record a loaded model's triangle count against the budget: over budget warns once per URL and lands in
173
+ * {@link heavyModels}; under budget is silent. Split out so the wiring is unit-testable without a GL context.
174
+ * @internal
175
+ */
176
+ export function recordModelTriangles(url, triangles, overrides = {}) {
177
+ if (triangles <= modelTriangleBudget)
178
+ return;
179
+ const record = { url, triangles, budget: modelTriangleBudget };
180
+ heavyModelRecords.push(record);
181
+ if (warnedHeavyUrls.has(url))
182
+ return;
183
+ warnedHeavyUrls.add(url);
184
+ (overrides.warn ?? warnHeavyModel)(record);
185
+ }
186
+ /**
187
+ * Every model the shared loader resolved whose triangle count exceeds the budget, in load order. A heavy
188
+ * prop is invisible in a screenshot and only shows up as a slow frame, so the loader measures each GLB as it
189
+ * lands and this exposes the verdict for `debug_snapshot` probes and smoke tests to assert against.
190
+ * @capability heavy-models models over the per-model triangle budget, for perf probes and smoke assertions
191
+ */
192
+ export function heavyModels() {
193
+ return heavyModelRecords;
194
+ }
195
+ /** Drops the heavy-model list and the once-per-URL warning memory. For tests and between capture runs. @internal */
196
+ export function clearHeavyModels() {
197
+ heavyModelRecords.length = 0;
198
+ warnedHeavyUrls.clear();
199
+ }
143
200
  /**
144
201
  * A {@link GLTFLoader} whose success path is unchanged but whose failures degrade
145
202
  * gracefully: when a load errors, it probes the URL and — for a diagnosed broken
@@ -151,7 +208,10 @@ export function clearModelLoadFallbacks() {
151
208
  */
152
209
  export class DiagnosticGLTFLoader extends GLTFLoader {
153
210
  load(url, onLoad, onProgress, onError) {
154
- super.load(url, onLoad, onProgress, (event) => {
211
+ super.load(url, (gltf) => {
212
+ recordModelTriangles(url, countSceneTriangles(gltf.scene));
213
+ onLoad(gltf);
214
+ }, onProgress, (event) => {
155
215
  if (onError === undefined)
156
216
  return;
157
217
  // Probe the URL the loader actually fetched (base-resolved), not the raw input.
@@ -163,6 +223,7 @@ const DEFAULT_DRACO_DECODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/e
163
223
  const DEFAULT_KTX2_TRANSCODER_PATH = "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/libs/basis/";
164
224
  let configuredModelLoaders;
165
225
  let ktx2Loader;
226
+ const ktx2DetectedRenderers = new WeakMap();
166
227
  /** Configures shared Draco and KTX2 loaders. Repeated calls with the same paths are no-ops. */
167
228
  export function configureModelLoaders(options = {}) {
168
229
  const next = {
@@ -182,7 +243,15 @@ export function configureModelLoaders(options = {}) {
182
243
  /** Detects GPU support for the configured KTX2 transcoder after a renderer exists. */
183
244
  export function detectKtx2Support(renderer) {
184
245
  configureModelLoaders();
246
+ if (ktx2DetectedRenderers.get(renderer) === ktx2Loader)
247
+ return;
185
248
  ktx2Loader?.detectSupport(renderer);
249
+ ktx2DetectedRenderers.set(renderer, ktx2Loader);
250
+ }
251
+ /** Shared transcoder for authored material maps; GPU support is detected by the canvas. @internal */
252
+ export function materialKtx2Loader() {
253
+ configureModelLoaders();
254
+ return ktx2Loader;
186
255
  }
187
256
  // LoadingManager reports queue transitions, not per-item events: `onStart` fires when the
188
257
  // queue goes busy and `onLoad` when it drains (errors included, via itemEnd), so a flag plus
@@ -0,0 +1,6 @@
1
+ import type { Object3D } from "three";
2
+ import { type RigNodeDiagnostic } from "./rigNode.js";
3
+ /** Order and duplicate references do not change a model instance's visibility selection. @internal */
4
+ export declare function modelHiddenNodesKey(names?: readonly string[]): string;
5
+ /** Apply visibility only to uniquely resolved nodes on an owned clone, retaining imported hidden state and rig hierarchy. @internal */
6
+ export declare function applyModelNodeVisibility(root: Object3D, names?: readonly string[]): RigNodeDiagnostic[];
@@ -0,0 +1,18 @@
1
+ import { parseModelHiddenNodes } from "@jgengine/core/editor/modelNodeVisibility";
2
+ import { resolveRigNode } from "./rigNode.js";
3
+ /** Order and duplicate references do not change a model instance's visibility selection. @internal */
4
+ export function modelHiddenNodesKey(names = []) {
5
+ return JSON.stringify([...new Set(names)].sort());
6
+ }
7
+ /** Apply visibility only to uniquely resolved nodes on an owned clone, retaining imported hidden state and rig hierarchy. @internal */
8
+ export function applyModelNodeVisibility(root, names = []) {
9
+ const diagnostics = [];
10
+ for (const name of new Set(parseModelHiddenNodes(names))) {
11
+ const resolved = resolveRigNode(root, name);
12
+ if (resolved.node === undefined)
13
+ diagnostics.push(resolved.diagnostic);
14
+ else
15
+ resolved.node.visible = false;
16
+ }
17
+ return diagnostics;
18
+ }
@@ -1,13 +1,30 @@
1
1
  import * as THREE from "three";
2
2
  import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
3
+ import type { ModelConfig } from "@jgengine/core/game/playableGame";
4
+ import { type MeasuredLocalBounds } from "./measureBounds.js";
3
5
  export declare const PAINT_TEXTURE_SIZE = 512;
4
6
  /** Shadow participation applied to every mesh of a cloned model; mirrors `ModelConfig.shadows`. */
5
7
  export type ModelShadowMode = "cast" | "receive" | "both" | "none";
6
- /** @internal */
8
+ /** Register an instance replacement without transferring borrowed geometry or textures. @internal */
9
+ export declare function ownModelMaterial(root: THREE.Object3D, material: THREE.Material): boolean;
10
+ /** Resolve placement from bind bounds in the frame containing the imported root, shared by rendering and collider measurement. @internal */
11
+ export declare function modelPlacementTransform(root: THREE.Object3D, model: ModelConfig, bindBounds?: MeasuredLocalBounds | null): {
12
+ scale: number;
13
+ position: [number, number, number];
14
+ };
15
+ /**
16
+ * Clone a model with independent pose and materials, retaining shared geometry and textures.
17
+ * @capability model-instance clone a loaded model for independent styling and animation
18
+ */
7
19
  export declare function cloneModelScene(source: THREE.Object3D, options?: {
8
20
  cloneMaterials?: boolean;
9
21
  shadows?: ModelShadowMode;
10
22
  }): THREE.Object3D;
23
+ /**
24
+ * Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
25
+ * @capability model-instance release a cloned model without disposing cached geometry or textures
26
+ */
27
+ export declare function disposeModelScene(root: THREE.Object3D): void;
11
28
  /** @internal */
12
29
  export declare function disposeClonedMaterials(root: THREE.Object3D): void;
13
30
  /** @internal */
@@ -1,25 +1,103 @@
1
1
  import * as THREE from "three";
2
2
  import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
3
+ import { measureLocalBounds } from "./measureBounds.js";
4
+ import { copyModelBindPose } from "./modelBindPose.js";
3
5
  export const PAINT_TEXTURE_SIZE = 512;
4
- /** @internal */
6
+ const modelResources = new WeakMap();
7
+ /** Register an instance replacement without transferring borrowed geometry or textures. @internal */
8
+ export function ownModelMaterial(root, material) {
9
+ const resources = modelResources.get(root);
10
+ if (resources === undefined)
11
+ return false;
12
+ resources.materials.add(material);
13
+ return true;
14
+ }
15
+ /** Resolve placement from bind bounds in the frame containing the imported root, shared by rendering and collider measurement. @internal */
16
+ export function modelPlacementTransform(root, model, bindBounds) {
17
+ let scale = model.scale ?? 1;
18
+ let minY = 0;
19
+ let centerX = 0;
20
+ let centerZ = 0;
21
+ const bounds = model.targetHeight === undefined ? null : bindBounds === undefined ? measureLocalBounds(root) : bindBounds;
22
+ const height = bounds === null ? 0 : bounds.max[1] - bounds.min[1];
23
+ if (bounds !== null && Number.isFinite(height) && height > 0) {
24
+ scale *= model.targetHeight / height;
25
+ minY = bounds.min[1];
26
+ centerX = (bounds.min[0] + bounds.max[0]) / 2;
27
+ centerZ = (bounds.min[2] + bounds.max[2]) / 2;
28
+ }
29
+ else if ((model.anchor ?? "center") === "center" && model.dims !== undefined) {
30
+ minY = model.dims.minY;
31
+ centerX = model.dims.center.x;
32
+ centerZ = model.dims.center.z;
33
+ }
34
+ return { scale, position: [-scale * centerX, (model.y ?? 0) - scale * minY, -scale * centerZ] };
35
+ }
36
+ /**
37
+ * Clone a model with independent pose and materials, retaining shared geometry and textures.
38
+ * @capability model-instance clone a loaded model for independent styling and animation
39
+ */
5
40
  export function cloneModelScene(source, options) {
6
41
  const clone = cloneSkinned(source);
42
+ copyModelBindPose(source, clone);
7
43
  const shadows = options?.shadows ?? "both";
8
44
  const cloneMaterials = options?.cloneMaterials !== false;
45
+ const materials = new Map();
46
+ const sourceSkeletons = [];
47
+ source.traverse((node) => {
48
+ if (node.isSkinnedMesh === true)
49
+ sourceSkeletons.push(node.skeleton);
50
+ });
51
+ const skeletons = new Map();
52
+ let skinnedIndex = 0;
53
+ const cloneMaterial = (source) => {
54
+ let material = materials.get(source);
55
+ if (material === undefined) {
56
+ material = source.clone();
57
+ material.onBeforeCompile = source.onBeforeCompile;
58
+ material.customProgramCacheKey = source.customProgramCacheKey;
59
+ materials.set(source, material);
60
+ }
61
+ return material;
62
+ };
9
63
  clone.traverse((node) => {
10
64
  const mesh = node;
11
65
  if (!mesh.isMesh)
12
66
  return;
67
+ if (mesh.isSkinnedMesh === true) {
68
+ const skinned = mesh;
69
+ const sourceSkeleton = sourceSkeletons[skinnedIndex++];
70
+ const shared = skeletons.get(sourceSkeleton);
71
+ if (shared === undefined)
72
+ skeletons.set(sourceSkeleton, skinned.skeleton);
73
+ else
74
+ skinned.skeleton = shared;
75
+ }
13
76
  mesh.castShadow = shadows === "cast" || shadows === "both";
14
77
  mesh.receiveShadow = shadows === "receive" || shadows === "both";
15
78
  if (!cloneMaterials)
16
79
  return;
17
80
  mesh.material = Array.isArray(mesh.material)
18
- ? mesh.material.map((material) => material.clone())
19
- : mesh.material.clone();
81
+ ? mesh.material.map(cloneMaterial)
82
+ : cloneMaterial(mesh.material);
20
83
  });
84
+ modelResources.set(clone, { materials: new Set(materials.values()), skeletons: new Set(skeletons.values()) });
21
85
  return clone;
22
86
  }
87
+ /**
88
+ * Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
89
+ * @capability model-instance release a cloned model without disposing cached geometry or textures
90
+ */
91
+ export function disposeModelScene(root) {
92
+ const resources = modelResources.get(root);
93
+ if (resources === undefined)
94
+ return;
95
+ for (const material of resources.materials)
96
+ material.dispose();
97
+ // Effect replay can recreate a bone texture on this same scene, so retain its ownership record.
98
+ for (const skeleton of resources.skeletons)
99
+ skeleton.dispose();
100
+ }
23
101
  /** @internal */
24
102
  export function disposeClonedMaterials(root) {
25
103
  const seen = new Set();
@@ -0,0 +1,35 @@
1
+ import * as THREE from "three";
2
+ /** Sampling resolution; lower resolutions change thin geometry and alpha-mask raster coverage. */
3
+ export interface ProjectedOverdrawOptions {
4
+ width?: number;
5
+ height?: number;
6
+ }
7
+ /** Projected mesh-fragment overlap, with depth rejection disabled; this is not production early-Z overdraw or GPU timing. */
8
+ export interface ProjectedOverdrawReport {
9
+ width: number;
10
+ height: number;
11
+ sampledPixels: number;
12
+ coveredPixels: number;
13
+ fragmentSamples: number;
14
+ mean: number;
15
+ meanCovered: number;
16
+ max: number;
17
+ meshes: number;
18
+ materialSlots: number;
19
+ /** Native opaque render-queue slots, including masked slots. */
20
+ opaqueSlots: number;
21
+ /** Slots with alphaTest/alphaHash; this can overlap queue counts. */
22
+ maskedSlots: number;
23
+ blendedSlots: number;
24
+ transmissionSlots: number;
25
+ caveats: readonly string[];
26
+ }
27
+ /** Convert finite nonnegative RGBA32F sample counts to overlap statistics. @internal */
28
+ export declare function summarizeProjectedOverdraw(pixels: Float32Array, width: number, height: number): Pick<ProjectedOverdrawReport, "sampledPixels" | "coveredPixels" | "fragmentSamples" | "mean" | "meanCovered" | "max">;
29
+ /**
30
+ * Opt in to a synchronous offscreen count of projected mesh fragments. Native PBR/unlit vertex transforms and alpha discard remain; depth rejection, shadow/refraction passes and postprocessing do not participate.
31
+ * Blended texels count equally, including zero opacity when native alphaTest/alphaHash does not discard them. Pure shader-modifying onBeforeCompile callbacks are retained; render callbacks, custom shader materials, stencil and alpha-to-coverage are rejected.
32
+ * The float readback stalls the GPU. Temporary materials/target are disposed and renderer draw counters/state restored; borrowed assets may become resident while measured.
33
+ * @capability projected-overdraw sample actual mesh/camera fragment overlap with native coverage and explicit depth/blending/transmission limits
34
+ */
35
+ export declare function measureProjectedOverdraw(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, options?: ProjectedOverdrawOptions): ProjectedOverdrawReport;
@@ -0,0 +1,204 @@
1
+ import * as THREE from "three";
2
+ function supported(material) {
3
+ const flags = material;
4
+ return flags.isMeshStandardMaterial === true || flags.isMeshBasicMaterial === true;
5
+ }
6
+ /** Convert finite nonnegative RGBA32F sample counts to overlap statistics. @internal */
7
+ export function summarizeProjectedOverdraw(pixels, width, height) {
8
+ if (pixels.length !== width * height * 4)
9
+ throw new Error("Projected-overdraw readback dimensions do not match the RGBA buffer");
10
+ let coveredPixels = 0, fragmentSamples = 0, max = 0;
11
+ for (let index = 0; index < pixels.length; index += 4) {
12
+ const count = pixels[index];
13
+ if (!Number.isFinite(count) || count < 0)
14
+ throw new Error("Projected-overdraw float readback returned invalid samples");
15
+ if (count > 0)
16
+ coveredPixels++;
17
+ fragmentSamples += count;
18
+ max = Math.max(max, count);
19
+ }
20
+ const sampledPixels = width * height;
21
+ return { sampledPixels, coveredPixels, fragmentSamples, mean: fragmentSamples / sampledPixels, meanCovered: coveredPixels === 0 ? 0 : fragmentSamples / coveredPixels, max };
22
+ }
23
+ function countMaterial(source) {
24
+ const material = source.clone();
25
+ const compile = source.onBeforeCompile;
26
+ const key = source.customProgramCacheKey();
27
+ material.onBeforeCompile = (shader, renderer) => {
28
+ compile.call(material, shader, renderer);
29
+ const tail = /#include <dithering_fragment>(\s*})\s*$/;
30
+ if (!tail.test(shader.fragmentShader))
31
+ throw new Error("Projected overdraw requires the native final fragment chunk; this shader callback changes the fragment main tail");
32
+ shader.fragmentShader = shader.fragmentShader.replace(tail, "#include <dithering_fragment>\ngl_FragColor = vec4(1.0);$1");
33
+ };
34
+ material.customProgramCacheKey = () => `${key}|jg-projected-overdraw:v1`;
35
+ material.transparent = true;
36
+ material.blending = THREE.CustomBlending;
37
+ material.blendEquation = THREE.AddEquation;
38
+ material.blendSrc = THREE.OneFactor;
39
+ material.blendDst = THREE.OneFactor;
40
+ material.blendEquationAlpha = THREE.AddEquation;
41
+ material.blendSrcAlpha = THREE.OneFactor;
42
+ material.blendDstAlpha = THREE.OneFactor;
43
+ material.depthTest = false;
44
+ material.depthWrite = false;
45
+ material.colorWrite = true;
46
+ material.forceSinglePass = true;
47
+ material.toneMapped = false;
48
+ material.premultipliedAlpha = false;
49
+ material.alphaToCoverage = false;
50
+ if (material.isMeshPhysicalMaterial)
51
+ material.transmission = 0;
52
+ return material;
53
+ }
54
+ /**
55
+ * Opt in to a synchronous offscreen count of projected mesh fragments. Native PBR/unlit vertex transforms and alpha discard remain; depth rejection, shadow/refraction passes and postprocessing do not participate.
56
+ * Blended texels count equally, including zero opacity when native alphaTest/alphaHash does not discard them. Pure shader-modifying onBeforeCompile callbacks are retained; render callbacks, custom shader materials, stencil and alpha-to-coverage are rejected.
57
+ * The float readback stalls the GPU. Temporary materials/target are disposed and renderer draw counters/state restored; borrowed assets may become resident while measured.
58
+ * @capability projected-overdraw sample actual mesh/camera fragment overlap with native coverage and explicit depth/blending/transmission limits
59
+ */
60
+ export function measureProjectedOverdraw(renderer, scene, camera, options = {}) {
61
+ const width = options.width ?? 128;
62
+ const height = options.height ?? 128;
63
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 2048 || height > 2048)
64
+ throw new Error("Projected-overdraw resolution must contain integer dimensions from 1 to 2048");
65
+ if (!renderer.extensions.has("EXT_color_buffer_float") || !renderer.extensions.has("EXT_float_blend"))
66
+ throw new Error("Projected overdraw requires RGBA32F rendering/readback and EXT_float_blend; this renderer cannot provide a fragment count");
67
+ if (camera.isArrayCamera)
68
+ throw new Error("Projected overdraw requires one camera projection; measure array-camera views separately");
69
+ if (scene.overrideMaterial !== null)
70
+ throw new Error("Projected overdraw does not support an active scene overrideMaterial");
71
+ const meshes = [];
72
+ const originals = new Map();
73
+ const materials = new Set();
74
+ let materialSlots = 0, opaqueSlots = 0, maskedSlots = 0, blendedSlots = 0, transmissionSlots = 0;
75
+ if (scene.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender || scene.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
76
+ throw new Error("Projected overdraw rejects scene render callbacks");
77
+ scene.traverseVisible(node => {
78
+ if (!node.layers.test(camera.layers))
79
+ return;
80
+ const flags = node;
81
+ if (flags.geometry?.drawRange.count === 0 || (flags.isInstancedMesh && flags.count === 0))
82
+ return;
83
+ if (flags.material !== undefined && (Array.isArray(flags.material) ? flags.material : [flags.material]).every(material => !material.visible))
84
+ return;
85
+ if (flags.isPoints || flags.isLine || flags.isSprite || flags.isBatchedMesh)
86
+ throw new Error(`Projected overdraw does not support ${node.type}`);
87
+ if (!flags.isMesh)
88
+ return;
89
+ if (node.onBeforeRender !== THREE.Object3D.prototype.onBeforeRender || node.onAfterRender !== THREE.Object3D.prototype.onAfterRender)
90
+ throw new Error(`Projected overdraw rejects render callbacks on ${node.name || node.type}`);
91
+ meshes.push(flags);
92
+ originals.set(flags, flags.material);
93
+ for (const material of Array.isArray(flags.material) ? flags.material : [flags.material]) {
94
+ if (!material.visible)
95
+ continue;
96
+ if (!supported(material))
97
+ throw new Error(`Projected overdraw requires native PBR or unlit material; ${material.name || material.type} is unsupported`);
98
+ if (material.onBeforeRender !== THREE.Material.prototype.onBeforeRender)
99
+ throw new Error(`Projected overdraw rejects material render callbacks on ${material.name || material.type}`);
100
+ if (material.stencilWrite || material.alphaToCoverage)
101
+ throw new Error(`Projected overdraw cannot reproduce stencil or multisample alpha coverage on ${material.name || material.type}`);
102
+ materials.add(material);
103
+ materialSlots++;
104
+ if (material.alphaTest > 0 || material.alphaHash)
105
+ maskedSlots++;
106
+ if (material.transparent)
107
+ blendedSlots++;
108
+ else
109
+ opaqueSlots++;
110
+ if (material.transmission > 0)
111
+ transmissionSlots++;
112
+ }
113
+ });
114
+ const target = new THREE.WebGLRenderTarget(width, height, { type: THREE.FloatType, format: THREE.RGBAFormat, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, depthBuffer: false, stencilBuffer: false, generateMipmaps: false });
115
+ target.texture.colorSpace = THREE.NoColorSpace;
116
+ const replacements = new Map();
117
+ const userData = new Map([...materials].map(material => [material, { object: material.userData, entries: { ...material.userData } }]));
118
+ const context = renderer.getContext();
119
+ const state = {
120
+ target: renderer.getRenderTarget(), face: renderer.getActiveCubeFace(), mip: renderer.getActiveMipmapLevel(),
121
+ viewport: renderer.getCurrentViewport(new THREE.Vector4()),
122
+ scissor: new THREE.Vector4().fromArray(context.getParameter(context.SCISSOR_BOX)), scissorTest: context.getParameter(context.SCISSOR_TEST),
123
+ clearColor: renderer.getClearColor(new THREE.Color()), clearAlpha: renderer.getClearAlpha(), autoClear: renderer.autoClear,
124
+ autoClearColor: renderer.autoClearColor, autoClearDepth: renderer.autoClearDepth, autoClearStencil: renderer.autoClearStencil,
125
+ xr: renderer.xr.enabled, shadows: renderer.shadowMap.enabled, infoAutoReset: renderer.info.autoReset, render: { ...renderer.info.render },
126
+ background: scene.background, fog: scene.fog, shaderCheck: renderer.debug.checkShaderErrors, shaderError: renderer.debug.onShaderError,
127
+ };
128
+ try {
129
+ const shaderErrors = [];
130
+ renderer.debug.checkShaderErrors = true;
131
+ renderer.debug.onShaderError = (gl, program, vertex, fragment) => { shaderErrors.push([gl.getProgramInfoLog(program), gl.getShaderInfoLog(vertex), gl.getShaderInfoLog(fragment)].filter(Boolean).join("\n")); };
132
+ renderer.xr.enabled = false;
133
+ renderer.shadowMap.enabled = false;
134
+ renderer.info.autoReset = false;
135
+ renderer.autoClear = false;
136
+ renderer.setRenderTarget(target);
137
+ renderer.setClearColor(0x000000, 0.25);
138
+ renderer.clear(true, false, false);
139
+ const probe = new Float32Array(4).fill(NaN);
140
+ renderer.readRenderTargetPixels(target, 0, 0, 1, 1, probe);
141
+ if (!Number.isFinite(probe[3]) || Math.abs(probe[3] - 0.25) > 0.001)
142
+ throw new Error("Projected overdraw RGBA32F readback is unsupported or incomplete");
143
+ renderer.setClearColor(0x000000, 0);
144
+ renderer.clear(true, false, false);
145
+ for (const source of materials)
146
+ replacements.set(source, countMaterial(source));
147
+ for (const mesh of meshes) {
148
+ const source = originals.get(mesh);
149
+ mesh.material = Array.isArray(source) ? source.map(material => replacements.get(material) ?? material) : replacements.get(source) ?? source;
150
+ }
151
+ scene.background = null;
152
+ scene.fog = null;
153
+ renderer.render(scene, camera);
154
+ if (shaderErrors.length > 0)
155
+ throw new Error(`Projected-overdraw shader compilation failed: ${shaderErrors.join("\n")}`);
156
+ const pixels = new Float32Array(width * height * 4).fill(NaN);
157
+ renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
158
+ return { width, height, ...summarizeProjectedOverdraw(pixels, width, height), meshes: meshes.length, materialSlots, opaqueSlots, maskedSlots, blendedSlots, transmissionSlots,
159
+ caveats: ["Depth rejection is disabled: projected overlap is an upper bound on production opaque shading, not early-Z overdraw.", "Native alphaTest/alphaHash discard is retained at this sampled resolution; blended texels count equally, including zero opacity.", "Transmission refraction/background, shadow, postprocessing, sky and MSAA passes are excluded. No GPU timing is measured.", "Native vertex transforms and pure shader-modifying material callbacks remain; borrowed assets may be uploaded during this diagnostic."] };
160
+ }
161
+ finally {
162
+ for (const [mesh, material] of originals)
163
+ mesh.material = material;
164
+ for (const [material, saved] of userData) {
165
+ material.userData = saved.object;
166
+ for (const key of Object.keys(saved.object))
167
+ if (!(key in saved.entries))
168
+ delete saved.object[key];
169
+ Object.assign(saved.object, saved.entries);
170
+ }
171
+ scene.background = state.background;
172
+ scene.fog = state.fog;
173
+ renderer.xr.enabled = state.xr;
174
+ renderer.shadowMap.enabled = state.shadows;
175
+ renderer.autoClear = state.autoClear;
176
+ renderer.autoClearColor = state.autoClearColor;
177
+ renderer.autoClearDepth = state.autoClearDepth;
178
+ renderer.autoClearStencil = state.autoClearStencil;
179
+ renderer.info.autoReset = state.infoAutoReset;
180
+ renderer.debug.checkShaderErrors = state.shaderCheck;
181
+ renderer.debug.onShaderError = state.shaderError;
182
+ for (const material of replacements.values())
183
+ material.dispose();
184
+ target.dispose();
185
+ if (state.target === null)
186
+ renderer.setRenderTarget(null, state.face, state.mip);
187
+ else {
188
+ const viewport = state.target.viewport.clone(), scissor = state.target.scissor.clone(), scissorTest = state.target.scissorTest;
189
+ state.target.viewport.copy(state.viewport);
190
+ state.target.scissor.copy(state.scissor);
191
+ state.target.scissorTest = state.scissorTest;
192
+ try {
193
+ renderer.setRenderTarget(state.target, state.face, state.mip);
194
+ }
195
+ finally {
196
+ state.target.viewport.copy(viewport);
197
+ state.target.scissor.copy(scissor);
198
+ state.target.scissorTest = scissorTest;
199
+ }
200
+ }
201
+ renderer.setClearColor(state.clearColor, state.clearAlpha);
202
+ Object.assign(renderer.info.render, state.render);
203
+ }
204
+ }
@@ -1,5 +1,6 @@
1
- import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
1
+ import type { AssetCatalog, ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
2
2
  import type { ModelConfig } from "@jgengine/core/game/playableGame";
3
+ import type { SceneObject } from "@jgengine/core/scene/objectStore";
3
4
  import type { GameContextModels } from "@jgengine/core/runtime/gameContext";
4
5
  export interface ModelResolveContext {
5
6
  seam: "entityModels" | "objectModels" | "scatterModels";
@@ -16,6 +17,14 @@ export declare function resolveModel(value: string | ModelConfig | undefined, as
16
17
  * @internal
17
18
  */
18
19
  export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
20
+ /** @internal */
21
+ export interface ObjectModelCacheEntry {
22
+ input: string | ModelConfig | undefined;
23
+ ref: ModelAssetRef | null;
24
+ model: ModelConfig | undefined;
25
+ }
26
+ /** Resolve a live object's catalog and animation, retaining unchanged base model identities. @internal */
27
+ export declare function resolveObjectModel(object: SceneObject, models: Record<string, string | ModelConfig> | undefined, assets: AssetCatalog, cache: Map<string, ObjectModelCacheEntry>): ModelConfig | undefined;
19
28
  /**
20
29
  * Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
21
30
  * so mounts receive fully-resolved `ModelConfig`s.
@@ -45,7 +54,8 @@ export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<stri
45
54
  * The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
46
55
  * object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
47
56
  * 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.
57
+ * reports it), passes inline configs through. Unmapped objects resolve their own catalog id,
58
+ * matching WorldScene; `undefined` when both the catalog and model maps are empty.
49
59
  * @internal
50
60
  */
51
61
  export declare function contextModels(playable: {