@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
@@ -0,0 +1,28 @@
1
+ import { actionContextStack, playControlsActive } from "@jgengine/core/game/controlGate";
2
+ /** Clears held input synchronously on play-gate transitions; ordinary context swaps retain it. @internal */
3
+ export function attachShellControlSuspension({ ctx, tracker, pointerAxisRef, analogRef, primaryClickRef, cameraDraggingRef, f2HeldRef, }) {
4
+ const reset = () => {
5
+ tracker.reset();
6
+ pointerAxisRef.current = null;
7
+ analogRef.current = null;
8
+ primaryClickRef.current = false;
9
+ cameraDraggingRef.current = false;
10
+ f2HeldRef.current = false;
11
+ ctx.input.publish([]);
12
+ ctx.input.publishPointer(null);
13
+ ctx.input.publishAnalog(null);
14
+ };
15
+ let active = playControlsActive(ctx);
16
+ if (!active)
17
+ reset();
18
+ const changed = () => {
19
+ const next = playControlsActive(ctx);
20
+ if (next === active)
21
+ return;
22
+ active = next;
23
+ reset();
24
+ };
25
+ const detachStack = actionContextStack(ctx).subscribe(changed);
26
+ const detachContext = ctx.subscribe(changed);
27
+ return () => { detachStack(); detachContext(); };
28
+ }
@@ -0,0 +1,28 @@
1
+ import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
2
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
3
+ import type { InputFrame } from "@jgengine/core/runtime/hostedGameRunner";
4
+ import type { InputSink } from "./inputSink.js";
5
+ type Change = "discrete" | "reset" | "analog";
6
+ /** Observes the actual shell tracker without moving command edges or changing binding behavior. @internal */
7
+ export declare function observableShellTracker(tracker: ActionStateTracker<string>): {
8
+ isDown(action: string): boolean;
9
+ wasPressed(action: string): boolean;
10
+ endFrame(): void;
11
+ rebind(map: import("@jgengine/core/input/actionBindings").ActionStateBindingMap<string, string>): void;
12
+ actions(): readonly string[];
13
+ handleDown(code: string): string | null;
14
+ handleUp(code: string): string | null;
15
+ reset(): void;
16
+ notify: (change: Change, presses?: NonNullable<InputFrame["presses"]>) => void;
17
+ subscribe(listener: (change: Change, presses: NonNullable<InputFrame["presses"]>) => void): () => void;
18
+ };
19
+ /** Publishes discrete input above Canvas, so release and failure leases survive stalled or unmounted rendering. @internal */
20
+ export declare function attachShellInputPublication({ ctx, tracker, active, analog, pointer, sink }: {
21
+ ctx: GameContext;
22
+ tracker: ReturnType<typeof observableShellTracker>;
23
+ active: () => boolean;
24
+ analog: () => InputFrame["analog"];
25
+ pointer: () => InputFrame["pointer"];
26
+ sink: () => InputSink;
27
+ }): () => void;
28
+ export {};
@@ -0,0 +1,49 @@
1
+ import { heldActionsFor } from "./boundActionDispatch.js";
2
+ let lastPressSeq = 0;
3
+ /** Observes the actual shell tracker without moving command edges or changing binding behavior. @internal */
4
+ export function observableShellTracker(tracker) {
5
+ const listeners = new Set();
6
+ const notify = (change, presses = []) => { for (const listener of listeners)
7
+ listener(change, presses); };
8
+ return {
9
+ ...tracker,
10
+ handleDown(code) {
11
+ const result = tracker.handleDown(code);
12
+ if (result !== null) {
13
+ const now = typeof performance !== "undefined" ? performance.timeOrigin + performance.now() : Date.now();
14
+ lastPressSeq = Math.max(now, lastPressSeq + 0.001);
15
+ notify("discrete", [{ action: result, seq: lastPressSeq }]);
16
+ }
17
+ return result;
18
+ },
19
+ handleUp(code) { const result = tracker.handleUp(code); notify("discrete"); return result; },
20
+ reset() { tracker.reset(); notify("reset"); },
21
+ notify,
22
+ subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
23
+ };
24
+ }
25
+ const owners = new WeakMap();
26
+ /** Publishes discrete input above Canvas, so release and failure leases survive stalled or unmounted rendering. @internal */
27
+ export function attachShellInputPublication({ ctx, tracker, active, analog, pointer, sink }) {
28
+ const owner = {};
29
+ owners.set(ctx, owner);
30
+ const publish = (change, presses = []) => {
31
+ if (owners.get(ctx) !== owner)
32
+ return;
33
+ const enabled = active() && change !== "reset";
34
+ const axes = enabled ? analog() : null;
35
+ ctx.input.publish(enabled ? heldActionsFor(tracker, tracker.actions()) : [], enabled ? { pressed: presses.map(press => press.action) } : { reset: true });
36
+ ctx.input.publishPointer(enabled ? pointer() : null);
37
+ ctx.input.publishAnalog(axes ?? null);
38
+ const urgent = change !== "analog" || axes == null || Object.values(axes).every(value => value === 0);
39
+ sink().send({ held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog(), tick: ctx.sim.tick(), ...(enabled && presses.length > 0 ? { presses } : {}) }, { urgent, ...(!enabled ? { reset: true } : {}) });
40
+ };
41
+ const detach = tracker.subscribe(publish);
42
+ return () => {
43
+ detach();
44
+ if (owners.get(ctx) !== owner)
45
+ return;
46
+ publish("reset");
47
+ owners.delete(ctx);
48
+ };
49
+ }
@@ -0,0 +1,18 @@
1
+ import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
2
+ import type { observableShellTracker } from "./shellInputPublication.js";
3
+ type Pointer = {
4
+ pointerId: number;
5
+ button: number;
6
+ };
7
+ /** Canvas-owned physical buttons; DOM overlays never begin gameplay input. @internal */
8
+ export declare function shellPointerInput(tracker: Pick<ActionStateTracker<string>, "handleDown" | "handleUp">, active: () => boolean): {
9
+ down(event: Pointer, worldTarget: boolean): void;
10
+ up(event: Pointer): boolean;
11
+ /** Native mouse chords change buttons through pointermove, including releases outside the canvas. */
12
+ move(event: Pointer & {
13
+ buttons: number;
14
+ }, worldTarget: boolean): void;
15
+ cancel(pointerId?: number): void;
16
+ attachReset(source: Pick<ReturnType<typeof observableShellTracker>, "subscribe">, clearGesture: () => void): () => void;
17
+ };
18
+ export {};
@@ -0,0 +1,49 @@
1
+ // PointerEvent.buttons uses middle=4 and right=2, unlike PointerEvent.button.
2
+ const buttonMask = (button) => [1, 4, 2, 8, 16][button] ?? 0;
3
+ /** Canvas-owned physical buttons; DOM overlays never begin gameplay input. @internal */
4
+ export function shellPointerInput(tracker, active) {
5
+ const buttons = new Map();
6
+ const input = {
7
+ down(event, worldTarget) {
8
+ if (!worldTarget || !active() || buttons.has(event.button))
9
+ return;
10
+ const action = tracker.handleDown(`mouse${event.button}`);
11
+ buttons.set(event.button, { pointerId: event.pointerId, bound: action !== null });
12
+ },
13
+ up(event) {
14
+ const held = buttons.get(event.button);
15
+ if (held === undefined || held.pointerId !== event.pointerId)
16
+ return false;
17
+ buttons.delete(event.button);
18
+ tracker.handleUp(`mouse${event.button}`);
19
+ return held.bound;
20
+ },
21
+ /** Native mouse chords change buttons through pointermove, including releases outside the canvas. */
22
+ move(event, worldTarget) {
23
+ for (const [button, held] of buttons) {
24
+ if (held.pointerId === event.pointerId && (event.buttons & buttonMask(button)) === 0) {
25
+ input.up({ pointerId: event.pointerId, button });
26
+ }
27
+ }
28
+ if (event.button >= 0 && (event.buttons & buttonMask(event.button)) !== 0)
29
+ input.down(event, worldTarget);
30
+ },
31
+ cancel(pointerId) {
32
+ for (const [button, held] of buttons) {
33
+ if (pointerId !== undefined && held.pointerId !== pointerId)
34
+ continue;
35
+ buttons.delete(button);
36
+ tracker.handleUp(`mouse${button}`);
37
+ }
38
+ },
39
+ attachReset(source, clearGesture) {
40
+ return source.subscribe(change => {
41
+ if (change !== "reset")
42
+ return;
43
+ input.cancel();
44
+ clearGesture();
45
+ });
46
+ },
47
+ };
48
+ return input;
49
+ }
@@ -1,8 +1,10 @@
1
+ import type { MaterialAsset } from "@jgengine/core/material/materialAsset";
1
2
  import { type BuildingKitInstance } from "./buildingKitFit.js";
2
3
  /** Props for {@link BuildingKitBatch}: one resolved model URL and every kit instance bound to it. @internal */
3
4
  export interface BuildingKitBatchProps {
4
5
  url: string;
5
6
  instances: readonly BuildingKitInstance[];
7
+ materialAssets?: readonly MaterialAsset[];
6
8
  }
7
9
  /**
8
10
  * GPU-instances one kit model across every building part bound to it: the GLB loads once, its meshes
@@ -10,4 +12,4 @@ export interface BuildingKitBatchProps {
10
12
  * while the GLB loads — mount it inside a `<Suspense>` so one slow model never blanks the rest.
11
13
  * @internal
12
14
  */
13
- export declare function BuildingKitBatch({ url, instances }: BuildingKitBatchProps): import("react").JSX.Element;
15
+ export declare function BuildingKitBatch({ url, instances, materialAssets }: BuildingKitBatchProps): import("react").JSX.Element;
@@ -1,44 +1,29 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef } from "react";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
3
3
  import { useLoader } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
+ import { useModelMaterialTextures } from "../render/materialAsset.js";
5
6
  import { sharedGltfLoader } from "../render/modelLoad.js";
6
7
  import { buildScatterModelSources, disposeScatterModelSources, } from "../scatter/scatterModels.js";
7
- import { composeBuildingKitMatrix, measureBuildingKitModel, } from "./buildingKitFit.js";
8
+ import { measureBuildingKitModel, } from "./buildingKitFit.js";
9
+ import { useBuildingSurfaceTextures } from "./buildingSurface.js";
10
+ import { groupBuildingKitInstances, buildBuildingKitSources, validateBuildingKitSource } from "./buildingKitMaterials.js";
11
+ import { useBuildingChunkBudget } from "./BuildingRenderBudget.js";
12
+ import { applyBuildingChunk, partitionBuildingMatrices } from "./buildingSpatialBatch.js";
8
13
  function KitSourceInstances({ source, material, matrices, }) {
14
+ const chunks = useMemo(() => partitionBuildingMatrices(source.geometry, matrices, source.localMatrix), [source, matrices]);
15
+ return _jsx(_Fragment, { children: chunks.map((chunk) => _jsx(KitSourceChunk, { source: source, material: material, chunk: chunk }, chunk.key)) });
16
+ }
17
+ function KitSourceChunk({ source, material, chunk }) {
9
18
  const meshRef = useRef(null);
10
- useEffect(() => {
19
+ useBuildingChunkBudget(meshRef, chunk.bounds);
20
+ useLayoutEffect(() => {
11
21
  const mesh = meshRef.current;
12
22
  if (mesh === null)
13
23
  return;
14
- const composed = new THREE.Matrix4();
15
- for (let i = 0; i < matrices.length; i += 1) {
16
- composed.multiplyMatrices(matrices[i], source.localMatrix);
17
- mesh.setMatrixAt(i, composed);
18
- }
19
- mesh.count = matrices.length;
20
- mesh.instanceMatrix.needsUpdate = true;
21
- mesh.computeBoundingSphere();
22
- }, [matrices, source]);
23
- return (_jsx("instancedMesh", { ref: meshRef, args: [source.geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
24
- }
25
- function tintOne(material, color) {
26
- const clone = material.clone();
27
- const tintable = clone;
28
- if (tintable.color !== undefined)
29
- tintable.color.copy(color);
30
- return clone;
31
- }
32
- function tintMaterial(material, tint) {
33
- const color = new THREE.Color(tint);
34
- return Array.isArray(material) ? material.map((entry) => tintOne(entry, color)) : tintOne(material, color);
35
- }
36
- function disposeMaterial(material) {
37
- if (Array.isArray(material))
38
- for (const entry of material)
39
- entry.dispose();
40
- else
41
- material.dispose();
24
+ return applyBuildingChunk(mesh, chunk);
25
+ }, [chunk, source, material]);
26
+ return (_jsx("instancedMesh", { ref: meshRef, args: [source.geometry, material, chunk.matrices.length], castShadow: true, receiveShadow: true, dispose: null }, chunk.matrices.length));
42
27
  }
43
28
  /**
44
29
  * GPU-instances one kit model across every building part bound to it: the GLB loads once, its meshes
@@ -46,33 +31,27 @@ function disposeMaterial(material) {
46
31
  * while the GLB loads — mount it inside a `<Suspense>` so one slow model never blanks the rest.
47
32
  * @internal
48
33
  */
49
- export function BuildingKitBatch({ url, instances }) {
34
+ export function BuildingKitBatch({ url, instances, materialAssets }) {
50
35
  const gltf = useLoader(sharedGltfLoader, url);
36
+ useMemo(() => {
37
+ const checked = new Set();
38
+ for (const instance of instances)
39
+ if (!checked.has(instance.part)) {
40
+ validateBuildingKitSource(gltf.scene, instance.part, materialAssets);
41
+ checked.add(instance.part);
42
+ }
43
+ }, [gltf, instances, materialAssets]);
51
44
  const { sources, root } = useMemo(() => buildScatterModelSources(gltf.scene, { url }), [gltf, url]);
52
45
  useEffect(() => () => disposeScatterModelSources(root), [root]);
53
46
  const bounds = useMemo(() => measureBuildingKitModel(root), [root]);
54
- const groups = useMemo(() => {
55
- const byTint = new Map();
56
- for (const instance of instances) {
57
- const matrix = composeBuildingKitMatrix(instance, bounds, new THREE.Matrix4());
58
- const tint = instance.part.tint ?? "";
59
- const bucket = byTint.get(tint);
60
- if (bucket === undefined)
61
- byTint.set(tint, [matrix]);
62
- else
63
- bucket.push(matrix);
64
- }
65
- return [...byTint.entries()];
66
- }, [instances, bounds]);
67
- const tinted = useMemo(() => {
68
- const map = new Map();
69
- for (const [tint] of groups) {
70
- if (tint === "")
71
- continue;
72
- map.set(tint, sources.map((source) => tintMaterial(source.material, tint)));
73
- }
74
- return map;
75
- }, [groups, sources]);
76
- useEffect(() => () => tinted.forEach((list) => list.forEach(disposeMaterial)), [tinted]);
77
- return (_jsx(_Fragment, { children: groups.map(([tint, matrices]) => sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: tinted.get(tint)?.[index] ?? source.material, matrices: matrices }, `${tint}:${index}`)))) }));
47
+ const groups = useMemo(() => groupBuildingKitInstances(instances, bounds, materialAssets), [instances, bounds, materialAssets]);
48
+ return (_jsx(_Fragment, { children: groups.map((group) => (group.part.tint ?? "") === "" && group.part.material === undefined && !group.part.materialAssignments?.length ? (sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: source.material, matrices: group.matrices }, `${group.key}:${index}`)))) : (_jsx(StyledKitSources, { scene: gltf.scene, materialAssets: materialAssets, group: group }, group.key))) }));
49
+ }
50
+ function StyledKitSources({ scene, materialAssets, group }) {
51
+ useMemo(() => validateBuildingKitSource(scene, group.part, materialAssets), [scene, group.key]);
52
+ const textures = useBuildingSurfaceTextures(group.part.material);
53
+ const materialTextures = useModelMaterialTextures({ materialAssets, materialAssignments: group.part.materialAssignments });
54
+ const { sources, root } = useMemo(() => buildBuildingKitSources(scene, group.part, materialAssets, textures, materialTextures.assets), [scene, group.key, textures, materialTextures]);
55
+ useEffect(() => () => disposeScatterModelSources(root), [root]);
56
+ return (_jsx(_Fragment, { children: sources.map((source, index) => (_jsx(KitSourceInstances, { source: source, material: source.material, matrices: group.matrices }, index))) }));
78
57
  }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode, type RefObject } from "react";
2
+ import * as THREE from "three";
3
+ import type { VisibilityConfig } from "@jgengine/core/visibility/config";
4
+ /** Resolved shell distance policy for generated facade batches. @internal */
5
+ export interface BuildingRenderPolicy {
6
+ enabled: boolean;
7
+ minDistance: number;
8
+ maxDistance: number;
9
+ hysteresis: number;
10
+ }
11
+ /** Resolve existing visibility overrides over the already resolved graphics-tier distance. @internal */
12
+ export declare function resolveBuildingRenderPolicy(drawDistance: number, config?: VisibilityConfig, cullingDisabled?: boolean): BuildingRenderPolicy;
13
+ /** Forward the shell's resolved graphics distance without registering generated parts as scene objects. @internal */
14
+ export declare function BuildingRenderBudget({ drawDistance, visibility, cullingDisabled, children }: {
15
+ drawDistance: number;
16
+ visibility?: VisibilityConfig;
17
+ cullingDisabled?: boolean;
18
+ children: ReactNode;
19
+ }): import("react").JSX.Element;
20
+ /** Test world bounds; transforming the box first also conservatively encloses affine shear. @internal */
21
+ export declare function buildingChunkWithinBudget(bounds: THREE.Box3, matrixWorld: THREE.Matrix4, cameraPosition: THREE.Vector3, policy: BuildingRenderPolicy, previouslyVisible: boolean, worldBounds: THREE.Box3, worldSphere: THREE.Sphere, frustumSphere?: THREE.Sphere | null): boolean;
22
+ /** Distance-gate chunks only; Three retains independent main-camera and shadow-camera frustum tests. @internal */
23
+ export declare function useBuildingChunkBudget(meshRef: RefObject<THREE.InstancedMesh | null>, bounds: THREE.Box3): void;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo, useRef } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+ import { culledByDistance } from "@jgengine/core/visibility/distance";
6
+ import { DEFAULT_CULLING_SETTINGS } from "@jgengine/core/visibility/settings";
7
+ import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
8
+ const UNLIMITED = { enabled: false, minDistance: 0, maxDistance: Infinity, hysteresis: 0 };
9
+ const BuildingRenderContext = createContext(UNLIMITED);
10
+ /** Resolve existing visibility overrides over the already resolved graphics-tier distance. @internal */
11
+ export function resolveBuildingRenderPolicy(drawDistance, config, cullingDisabled = false) {
12
+ const culling = config?.culling;
13
+ const scene = config?.scene;
14
+ return {
15
+ // Scene callbacks address visibility rows; generated parts have no row to evaluate safely.
16
+ enabled: !cullingDisabled && config?.enabled !== false && culling?.enabled !== false && culling?.distanceCulling !== false
17
+ && scene?.alwaysVisible !== true && scene?.cullingDisabled !== true && scene?.customVisibility === undefined,
18
+ minDistance: scene?.minRenderDistance ?? culling?.defaultMinRenderDistance ?? DEFAULT_CULLING_SETTINGS.defaultMinRenderDistance,
19
+ maxDistance: scene?.maxRenderDistance ?? culling?.defaultMaxRenderDistance ?? drawDistance,
20
+ hysteresis: culling?.hysteresis ?? DEFAULT_CULLING_SETTINGS.hysteresis,
21
+ };
22
+ }
23
+ /** Forward the shell's resolved graphics distance without registering generated parts as scene objects. @internal */
24
+ export function BuildingRenderBudget({ drawDistance, visibility, cullingDisabled = false, children }) {
25
+ const policy = useMemo(() => resolveBuildingRenderPolicy(drawDistance, visibility, cullingDisabled), [drawDistance, visibility, cullingDisabled]);
26
+ return _jsx(BuildingRenderContext.Provider, { value: policy, children: children });
27
+ }
28
+ /** Test world bounds; transforming the box first also conservatively encloses affine shear. @internal */
29
+ export function buildingChunkWithinBudget(bounds, matrixWorld, cameraPosition, policy, previouslyVisible, worldBounds, worldSphere, frustumSphere) {
30
+ worldBounds.copy(bounds).applyMatrix4(matrixWorld).getBoundingSphere(worldSphere);
31
+ if (frustumSphere !== undefined && frustumSphere !== null) {
32
+ bounds.getCenter(frustumSphere.center);
33
+ const worldScale = matrixWorld.getMaxScaleOnAxis();
34
+ // Three transforms this sphere using the largest matrix column, which can underbound shear.
35
+ frustumSphere.radius = Math.max(bounds.min.distanceTo(bounds.max) / 2, worldScale === 0 ? 0 : worldSphere.radius / worldScale);
36
+ }
37
+ if (!policy.enabled)
38
+ return true;
39
+ const { center, radius } = worldSphere;
40
+ return !culledByDistance(cameraPosition.x, cameraPosition.y, cameraPosition.z, center.x, center.y, center.z, policy.minDistance, policy.maxDistance, radius, previouslyVisible ? policy.hysteresis : 0);
41
+ }
42
+ /** Distance-gate chunks only; Three retains independent main-camera and shadow-camera frustum tests. @internal */
43
+ export function useBuildingChunkBudget(meshRef, bounds) {
44
+ const policy = useContext(BuildingRenderContext);
45
+ const scratch = useRef({ box: new THREE.Box3(), sphere: new THREE.Sphere(), camera: new THREE.Vector3() });
46
+ useFrame(({ camera }) => {
47
+ const mesh = meshRef.current;
48
+ if (mesh === null)
49
+ return;
50
+ mesh.updateWorldMatrix(true, false);
51
+ camera.getWorldPosition(scratch.current.camera);
52
+ mesh.visible = buildingChunkWithinBudget(bounds, mesh.matrixWorld, scratch.current.camera, policy, mesh.visible, scratch.current.box, scratch.current.sphere, mesh.boundingSphere);
53
+ }, CAMERA_POST_FRAME_PRIORITY);
54
+ }
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import type { BuildingKitSlot } from "@jgengine/core/world/buildings";
2
+ import { type BuildingKitSlot, type BuildingSurface } from "@jgengine/core/world/buildings";
3
3
  import type { BuildingKit } from "@jgengine/core/world/buildingKit";
4
4
  export type BuildingFacade = "front" | "back" | "left" | "right" | "roof";
5
5
  export type BuildingPartKind = "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner";
@@ -17,20 +17,8 @@ export interface GeneratedBuildingData {
17
17
  id: string;
18
18
  parts: readonly BuildingPartPlacement[];
19
19
  }
20
- export interface BuildingMaterialPalette {
21
- wall?: string;
22
- window?: string;
23
- awning?: string;
24
- airConditioner?: string;
25
- clothesline?: string;
26
- storefront?: string;
27
- shutter?: string;
28
- storeSign?: string;
29
- roof?: string;
30
- roofProp?: string;
31
- guardrail?: string;
32
- corner?: string;
33
- }
20
+ /** Per part kind: a hex colour, or a colour plus tiled PBR maps (`BuildingSurfaceMaterial`). */
21
+ export type BuildingMaterialPalette = Partial<Record<BuildingPartKind, BuildingSurface>>;
34
22
  export interface BuildingKitRenderer {
35
23
  renderPart?: (part: BuildingPartPlacement) => ReactNode | undefined;
36
24
  }
@@ -1,9 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Suspense, useLayoutEffect, useMemo, useRef } from "react";
3
3
  import * as THREE from "three";
4
+ import { buildingSurfaceColor, resolveBuildingSurface, } from "@jgengine/core/world/buildings";
4
5
  import { useDisposable } from "../render/useDisposable.js";
5
6
  import { BuildingKitBatch } from "./BuildingKitBatch.js";
7
+ import { useBuildingChunkBudget } from "./BuildingRenderBudget.js";
8
+ import { applyBuildingChunk, partitionBuildingMatrices } from "./buildingSpatialBatch.js";
6
9
  import { bucketBuildingParts, normalFor, outwardOffset } from "./buildingKitFit.js";
10
+ import { applyBuildingSurface, useBuildingSurfaceTextures } from "./buildingSurface.js";
7
11
  const DEFAULT_PALETTE = {
8
12
  wall: "#83766a",
9
13
  window: "#8ecae6",
@@ -18,8 +22,12 @@ const DEFAULT_PALETTE = {
18
22
  guardrail: "#a8a29e",
19
23
  corner: "#6b6258",
20
24
  };
25
+ function surfaceFor(kind, palette) {
26
+ const surface = resolveBuildingSurface(palette?.[kind] ?? DEFAULT_PALETTE[kind]);
27
+ return surface.color === undefined ? { ...surface, color: DEFAULT_PALETTE[kind] } : surface;
28
+ }
21
29
  function colorFor(kind, palette) {
22
- return palette?.[kind] ?? DEFAULT_PALETTE[kind];
30
+ return buildingSurfaceColor(palette?.[kind], DEFAULT_PALETTE[kind]);
23
31
  }
24
32
  function materialFor(part, palette) {
25
33
  const color = colorFor(part.kind, palette);
@@ -34,31 +42,34 @@ function materialFor(part, palette) {
34
42
  }
35
43
  return _jsx("meshStandardMaterial", { color: color, roughness: 0.88, metalness: 0 });
36
44
  }
37
- function batchMaterialFor(kind, palette) {
38
- const color = colorFor(kind, palette);
39
- if (kind === "window" || kind === "storefront") {
40
- return new THREE.MeshPhysicalMaterial({ color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 });
41
- }
42
- if (kind === "storeSign") {
43
- return new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 });
44
- }
45
- if (kind === "roofProp") {
46
- return new THREE.MeshStandardMaterial({ color, roughness: 0.65, metalness: 0.1 });
47
- }
48
- return new THREE.MeshStandardMaterial({ color, roughness: 0.88, metalness: 0 });
45
+ function batchMaterialFor(kind, surface, textures) {
46
+ const color = surface.color ?? DEFAULT_PALETTE[kind];
47
+ const base = kind === "window" || kind === "storefront"
48
+ ? new THREE.MeshPhysicalMaterial({ color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 })
49
+ : kind === "storeSign"
50
+ ? new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 })
51
+ : kind === "roofProp"
52
+ ? new THREE.MeshStandardMaterial({ color, roughness: 0.65, metalness: 0.1 })
53
+ : new THREE.MeshStandardMaterial({ color, roughness: 0.88, metalness: 0 });
54
+ return applyBuildingSurface(base, surface, textures);
55
+ }
56
+ function BuildingKindBatch({ kind, matrices, palette, geometry }) {
57
+ const surface = useMemo(() => surfaceFor(kind, palette), [kind, palette]);
58
+ const textures = useBuildingSurfaceTextures(surface);
59
+ const material = useDisposable(() => batchMaterialFor(kind, surface, textures), [kind, surface, textures]);
60
+ const chunks = useMemo(() => partitionBuildingMatrices(geometry, matrices), [geometry, matrices]);
61
+ return _jsx(_Fragment, { children: chunks.map((chunk) => _jsx(BuildingChunk, { chunk: chunk, material: material, geometry: geometry }, chunk.key)) });
49
62
  }
50
- function BuildingKindBatch({ kind, matrices, palette, geometry, }) {
63
+ function BuildingChunk({ chunk, material, geometry }) {
51
64
  const meshRef = useRef(null);
52
- const material = useDisposable(() => batchMaterialFor(kind, palette), [kind, palette]);
65
+ useBuildingChunkBudget(meshRef, chunk.bounds);
53
66
  useLayoutEffect(() => {
54
67
  const mesh = meshRef.current;
55
68
  if (mesh === null)
56
69
  return;
57
- matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix));
58
- mesh.instanceMatrix.needsUpdate = true;
59
- mesh.computeBoundingSphere();
60
- }, [matrices, material]);
61
- return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
70
+ return applyBuildingChunk(mesh, chunk);
71
+ }, [chunk, geometry, material]);
72
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, chunk.matrices.length], castShadow: true, receiveShadow: true, dispose: null }, chunk.matrices.length));
62
73
  }
63
74
  export function InstancedBuildings({ buildings, palette, kit, resolveModelUrl, visibleKinds, }) {
64
75
  const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
@@ -68,7 +79,7 @@ export function InstancedBuildings({ buildings, palette, kit, resolveModelUrl, v
68
79
  }, [buildings, visibleKinds, kit, resolveModelUrl]);
69
80
  if (boxes.size === 0 && models.size === 0)
70
81
  return null;
71
- return (_jsxs("group", { children: [[...boxes.entries()].map(([kind, matrices]) => (_jsx(BuildingKindBatch, { kind: kind, matrices: matrices, palette: palette, geometry: geometry }, kind))), [...models.entries()].map(([url, instances]) => (_jsx(Suspense, { fallback: null, children: _jsx(BuildingKitBatch, { url: url, instances: instances }) }, url)))] }));
82
+ return (_jsxs("group", { children: [[...boxes.entries()].map(([kind, matrices]) => (_jsx(BuildingKindBatch, { kind: kind, matrices: matrices, palette: palette, geometry: geometry }, kind))), [...models.entries()].map(([url, instances]) => (_jsx(Suspense, { fallback: null, children: _jsx(BuildingKitBatch, { url: url, instances: instances, materialAssets: kit?.materialAssets }) }, url)))] }));
72
83
  }
73
84
  function BlockMesh({ part, palette }) {
74
85
  const [nx, nz] = normalFor(part.facade);
@@ -0,0 +1,20 @@
1
+ import * as THREE from "three";
2
+ import type { BuildingKitPart } from "@jgengine/core/world/buildingKit";
3
+ import { type MaterialAsset } from "@jgengine/core/material/materialAsset";
4
+ import type { MaterialOverrideTextures } from "../materialOverride.js";
5
+ import { buildScatterModelSources } from "../scatter/scatterModels.js";
6
+ import { type BuildingKitInstance, type BuildingKitModelBounds } from "./buildingKitFit.js";
7
+ /** The material values used by one kit variant, excluding unrelated document library entries. @internal */
8
+ export declare function buildingKitMaterialKey(part: BuildingKitPart, assets?: readonly MaterialAsset[]): string;
9
+ /** One material style shared by every fitted instance using it. @internal */
10
+ export interface BuildingKitStyleGroup {
11
+ key: string;
12
+ part: BuildingKitPart;
13
+ matrices: THREE.Matrix4[];
14
+ }
15
+ /** Group fitted instances while resolving each repeated variant's material identity only once. @internal */
16
+ export declare function groupBuildingKitInstances(instances: readonly BuildingKitInstance[], bounds: BuildingKitModelBounds, assets?: readonly MaterialAsset[]): BuildingKitStyleGroup[];
17
+ /** Validate named kit materials and native targets before any owned model or texture allocation. @internal */
18
+ export declare function validateBuildingKitSource(scene: THREE.Object3D, part: BuildingKitPart, assets?: readonly MaterialAsset[]): void;
19
+ /** Prepare one style's owned model materials before harvesting shared geometry for bounded instanced chunks. @internal */
20
+ export declare function buildBuildingKitSources(scene: THREE.Object3D, part: BuildingKitPart, assets?: readonly MaterialAsset[], surfaceTextures?: MaterialOverrideTextures, assetTextures?: ReadonlyMap<string, MaterialOverrideTextures>): ReturnType<typeof buildScatterModelSources>;
@@ -0,0 +1,70 @@
1
+ import * as THREE from "three";
2
+ import { validateMaterialAssignments } from "@jgengine/core/material/materialAsset";
3
+ import { applyMaterialAssignments, validateMaterialAssignmentTargets } from "../render/materialAsset.js";
4
+ import { buildScatterModelSources, disposeScatterModelSources } from "../scatter/scatterModels.js";
5
+ import { composeBuildingKitMatrix } from "./buildingKitFit.js";
6
+ import { applyBuildingSurface, surfaceKey } from "./buildingSurface.js";
7
+ /** The material values used by one kit variant, excluding unrelated document library entries. @internal */
8
+ export function buildingKitMaterialKey(part, assets = []) {
9
+ const ids = new Set(part.materialAssignments?.map(assignment => assignment.materialId));
10
+ return JSON.stringify([part.tint ?? "", surfaceKey(part.material), part.materialAssignments ?? [], assets.filter(asset => ids.has(asset.id))]);
11
+ }
12
+ /** Group fitted instances while resolving each repeated variant's material identity only once. @internal */
13
+ export function groupBuildingKitInstances(instances, bounds, assets = []) {
14
+ const byKey = new Map();
15
+ const styleKeys = new Map();
16
+ for (const instance of instances) {
17
+ const matrix = composeBuildingKitMatrix(instance, bounds, new THREE.Matrix4());
18
+ let key = styleKeys.get(instance.part);
19
+ if (key === undefined) {
20
+ key = buildingKitMaterialKey(instance.part, assets);
21
+ styleKeys.set(instance.part, key);
22
+ }
23
+ const bucket = byKey.get(key);
24
+ if (bucket === undefined)
25
+ byKey.set(key, { key, part: instance.part, matrices: [matrix] });
26
+ else
27
+ bucket.matrices.push(matrix);
28
+ }
29
+ return [...byKey.values()];
30
+ }
31
+ /** Validate named kit materials and native targets before any owned model or texture allocation. @internal */
32
+ export function validateBuildingKitSource(scene, part, assets = []) {
33
+ const assignments = part.materialAssignments ?? [];
34
+ const errors = validateMaterialAssignments(assignments, assets, { disallowedTextureRoles: ["height"] }).filter(item => item.severity === "error");
35
+ if (errors.length > 0)
36
+ throw new Error(`Building kit ${part.model}: ${errors.map(item => `${item.path}: ${item.message}`).join("; ")}`);
37
+ validateMaterialAssignmentTargets(scene, assets, assignments);
38
+ }
39
+ /** Prepare one style's owned model materials before harvesting shared geometry for bounded instanced chunks. @internal */
40
+ export function buildBuildingKitSources(scene, part, assets = [], surfaceTextures, assetTextures = new Map()) {
41
+ validateBuildingKitSource(scene, part, assets);
42
+ const prepared = buildScatterModelSources(scene, { url: part.model });
43
+ try {
44
+ const tint = part.tint === undefined || part.tint === "" ? undefined : new THREE.Color(part.tint);
45
+ prepared.root.traverseVisible(node => {
46
+ const mesh = node;
47
+ if (!mesh.isMesh)
48
+ return;
49
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
50
+ if (tint !== undefined && "color" in material)
51
+ material.color.copy(tint);
52
+ if (part.material !== undefined && material.isMeshStandardMaterial)
53
+ applyBuildingSurface(material, part.material, surfaceTextures);
54
+ }
55
+ });
56
+ if (part.materialAssignments?.length)
57
+ applyMaterialAssignments(prepared.root, assets, part.materialAssignments, assetTextures);
58
+ let index = 0;
59
+ prepared.root.traverseVisible(node => {
60
+ const mesh = node;
61
+ if (mesh.isMesh)
62
+ prepared.sources[index++].material = mesh.material;
63
+ });
64
+ return prepared;
65
+ }
66
+ catch (error) {
67
+ disposeScatterModelSources(prepared.root);
68
+ throw error;
69
+ }
70
+ }
@@ -0,0 +1,11 @@
1
+ import * as THREE from "three";
2
+ /** A spatially bounded group of final, geometry-local instance matrices. @internal */
3
+ export interface BuildingSpatialChunk {
4
+ key: string;
5
+ matrices: THREE.Matrix4[];
6
+ bounds: THREE.Box3;
7
+ }
8
+ /** Partition final geometry bounds without changing instance transforms or material groups. @internal */
9
+ export declare function partitionBuildingMatrices(geometry: THREE.BufferGeometry, matrices: readonly THREE.Matrix4[], sourceMatrix?: THREE.Matrix4, cellSize?: number): BuildingSpatialChunk[];
10
+ /** Install matrices and bounds; cleanup releases only the mesh's instance buffers, retaining shared assets. @internal */
11
+ export declare function applyBuildingChunk(mesh: THREE.InstancedMesh, chunk: BuildingSpatialChunk): () => void;