@jgengine/shell 0.19.0 → 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 (203) hide show
  1. package/CHANGELOG.md +346 -0
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +113 -15
  5. package/dist/Shell3dPresentation.d.ts +2 -2
  6. package/dist/Shell3dPresentation.js +49 -7
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +11 -1
  10. package/dist/ShellHudPresentation.js +20 -6
  11. package/dist/audio/audioEngine.d.ts +16 -0
  12. package/dist/audio/audioEngine.js +178 -89
  13. package/dist/audio/audioWire.js +2 -2
  14. package/dist/audio/synthEngine.d.ts +7 -1
  15. package/dist/audio/synthEngine.js +103 -38
  16. package/dist/camera/GameCameraRig.js +2 -1
  17. package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
  18. package/dist/camera/GameFirstPersonCamera.js +17 -13
  19. package/dist/camera/cameraRigs.js +8 -2
  20. package/dist/camera/chaseHeading.d.ts +9 -0
  21. package/dist/camera/chaseHeading.js +10 -0
  22. package/dist/camera/customMuzzle.d.ts +14 -0
  23. package/dist/camera/customMuzzle.js +68 -0
  24. package/dist/camera/index.d.ts +1 -0
  25. package/dist/camera/index.js +1 -0
  26. package/dist/camera/rigResolve.d.ts +3 -0
  27. package/dist/camera/rigResolve.js +11 -0
  28. package/dist/combat/ProjectileModels.d.ts +31 -0
  29. package/dist/combat/ProjectileModels.js +70 -0
  30. package/dist/commandSink.d.ts +1 -2
  31. package/dist/commandSink.js +3 -2
  32. package/dist/defineGame.d.ts +12 -5
  33. package/dist/defineGame.js +24 -9
  34. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  35. package/dist/devtools/DevtoolsOverlay.js +14 -168
  36. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  37. package/dist/devtools/DevtoolsRuntime.js +61 -0
  38. package/dist/devtools/agentBridge.js +1 -1
  39. package/dist/devtools/devtoolsReports.d.ts +68 -0
  40. package/dist/devtools/devtoolsReports.js +100 -0
  41. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  42. package/dist/devtools/latencyInstrumentation.js +11 -0
  43. package/dist/devtools/playtestProbe.d.ts +4 -0
  44. package/dist/devtools/playtestProbe.js +22 -0
  45. package/dist/drivers/FrameDriver.js +25 -21
  46. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  47. package/dist/drivers/HudOnlyDriver.js +32 -14
  48. package/dist/environment/EnvironmentScene.js +11 -10
  49. package/dist/inputSink.d.ts +6 -6
  50. package/dist/inputSink.js +60 -21
  51. package/dist/lazyShell3dPresentation.d.ts +9 -0
  52. package/dist/lazyShell3dPresentation.js +24 -0
  53. package/dist/materialOverride.d.ts +42 -0
  54. package/dist/materialOverride.js +95 -36
  55. package/dist/multiplayer.d.ts +33 -3
  56. package/dist/multiplayer.js +105 -22
  57. package/dist/pointer/pointerService.d.ts +2 -0
  58. package/dist/pointer/pointerService.js +12 -2
  59. package/dist/postfx/PostProcessing.d.ts +10 -0
  60. package/dist/postfx/PostProcessing.js +9 -41
  61. package/dist/postfx/gradeShader.d.ts +1 -1
  62. package/dist/postfx/gradeShader.js +30 -2
  63. package/dist/postfx/shadowPrepass.d.ts +3 -0
  64. package/dist/postfx/shadowPrepass.js +18 -0
  65. package/dist/presentationRecovery.d.ts +75 -0
  66. package/dist/presentationRecovery.js +204 -0
  67. package/dist/presentationRecovery3d.d.ts +4 -0
  68. package/dist/presentationRecovery3d.js +32 -0
  69. package/dist/presentationResolve.d.ts +8 -1
  70. package/dist/presentationResolve.js +5 -2
  71. package/dist/render/CascadedShadows.d.ts +7 -2
  72. package/dist/render/CascadedShadows.js +98 -20
  73. package/dist/render/EnvironmentLighting.js +74 -35
  74. package/dist/render/HairCards.d.ts +7 -0
  75. package/dist/render/HairCards.js +23 -0
  76. package/dist/render/SceneModels.js +51 -77
  77. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  78. package/dist/render/authoredSurfaceMaterial.js +108 -0
  79. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  80. package/dist/render/materialAppearanceSignals.js +58 -0
  81. package/dist/render/materialAsset.d.ts +98 -0
  82. package/dist/render/materialAsset.js +454 -0
  83. package/dist/render/measureBounds.js +16 -3
  84. package/dist/render/measureCollisionMesh.js +8 -1
  85. package/dist/render/modelAssets.d.ts +8 -0
  86. package/dist/render/modelAssets.js +43 -0
  87. package/dist/render/modelBindPose.d.ts +5 -0
  88. package/dist/render/modelBindPose.js +35 -0
  89. package/dist/render/modelLoad.d.ts +3 -0
  90. package/dist/render/modelLoad.js +9 -0
  91. package/dist/render/modelNodeVisibility.d.ts +6 -0
  92. package/dist/render/modelNodeVisibility.js +18 -0
  93. package/dist/render/modelRender.d.ts +18 -1
  94. package/dist/render/modelRender.js +81 -3
  95. package/dist/render/projectedOverdraw.d.ts +35 -0
  96. package/dist/render/projectedOverdraw.js +204 -0
  97. package/dist/render/resolveModel.d.ts +12 -2
  98. package/dist/render/resolveModel.js +21 -4
  99. package/dist/render/rigNode.d.ts +26 -0
  100. package/dist/render/rigNode.js +32 -0
  101. package/dist/render/spriteRender.d.ts +4 -0
  102. package/dist/render/spriteRender.js +5 -0
  103. package/dist/render/staticShapeInstances.d.ts +34 -0
  104. package/dist/render/staticShapeInstances.js +78 -0
  105. package/dist/render/useFootIk.js +76 -11
  106. package/dist/render/useModelAnimation.d.ts +28 -2
  107. package/dist/render/useModelAnimation.js +194 -69
  108. package/dist/render/useModelInstance.d.ts +25 -0
  109. package/dist/render/useModelInstance.js +43 -0
  110. package/dist/scatter/scatterModels.d.ts +3 -2
  111. package/dist/scatter/scatterModels.js +15 -7
  112. package/dist/scene/AuthoredScene.d.ts +14 -6
  113. package/dist/scene/AuthoredScene.js +30 -9
  114. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  115. package/dist/settings/SettingsMenu.js +11 -18
  116. package/dist/settings/SettingsRuntime.d.ts +5 -0
  117. package/dist/settings/SettingsRuntime.js +29 -2
  118. package/dist/shellControlSuspension.d.ts +18 -0
  119. package/dist/shellControlSuspension.js +28 -0
  120. package/dist/shellInputPublication.d.ts +28 -0
  121. package/dist/shellInputPublication.js +49 -0
  122. package/dist/shellPointerInput.d.ts +18 -0
  123. package/dist/shellPointerInput.js +49 -0
  124. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  125. package/dist/structures/BuildingKitBatch.js +34 -59
  126. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  127. package/dist/structures/BuildingRenderBudget.js +54 -0
  128. package/dist/structures/GeneratedBuilding.js +13 -8
  129. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  130. package/dist/structures/buildingKitMaterials.js +70 -0
  131. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  132. package/dist/structures/buildingSpatialBatch.js +45 -0
  133. package/dist/terrain/GrassField.d.ts +2 -1
  134. package/dist/terrain/GrassField.js +59 -5
  135. package/dist/terrain/grassGeometry.d.ts +10 -0
  136. package/dist/terrain/grassGeometry.js +55 -0
  137. package/dist/terrain/grassMaterial.js +2 -2
  138. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  139. package/dist/terrain/terrainDetailMaterial.js +7 -7
  140. package/dist/touch/TouchControlsOverlay.js +4 -4
  141. package/dist/useShellMultiplayerSync.js +4 -0
  142. package/dist/vfx/ParticleField.d.ts +23 -20
  143. package/dist/vfx/ParticleField.js +168 -50
  144. package/dist/visibility/CullingProvider.js +2 -62
  145. package/dist/visibility/cullingDriver.d.ts +9 -0
  146. package/dist/visibility/cullingDriver.js +68 -0
  147. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  148. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  149. package/dist/weather/DustField.d.ts +2 -1
  150. package/dist/weather/DustField.js +17 -3
  151. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  152. package/dist/weather/FireSpreadLayer.js +5 -11
  153. package/dist/weather/LightningStrike.d.ts +4 -1
  154. package/dist/weather/LightningStrike.js +13 -17
  155. package/dist/weather/RainField.d.ts +2 -1
  156. package/dist/weather/RainField.js +17 -3
  157. package/dist/weather/RainImpactField.d.ts +16 -0
  158. package/dist/weather/RainImpactField.js +89 -0
  159. package/dist/weather/SnowField.d.ts +2 -1
  160. package/dist/weather/SnowField.js +17 -3
  161. package/dist/weather/WeatherLayer.d.ts +11 -2
  162. package/dist/weather/WeatherLayer.js +30 -6
  163. package/dist/weather/index.d.ts +2 -0
  164. package/dist/weather/index.js +2 -0
  165. package/dist/weather/weatherMath.d.ts +2 -0
  166. package/dist/weather/weatherMath.js +8 -0
  167. package/dist/weather/weatherShelter.d.ts +18 -0
  168. package/dist/weather/weatherShelter.js +75 -0
  169. package/dist/weather/weatherUniforms.d.ts +40 -1
  170. package/dist/weather/weatherUniforms.js +29 -9
  171. package/dist/world/AuthoredFlocks.d.ts +22 -0
  172. package/dist/world/AuthoredFlocks.js +87 -0
  173. package/dist/world/StaticObjectModels.d.ts +8 -0
  174. package/dist/world/StaticObjectModels.js +75 -0
  175. package/dist/world/WorldHud.d.ts +14 -6
  176. package/dist/world/WorldHud.js +25 -12
  177. package/dist/world/WorldParticles.d.ts +10 -23
  178. package/dist/world/WorldParticles.js +209 -61
  179. package/dist/world/WorldScene.d.ts +3 -1
  180. package/dist/world/WorldScene.js +41 -26
  181. package/dist/world/staticObjectBatches.d.ts +35 -0
  182. package/dist/world/staticObjectBatches.js +163 -0
  183. package/dist/world/worldBarSamples.d.ts +37 -33
  184. package/dist/world/worldBarSamples.js +117 -75
  185. package/package.json +4 -4
  186. package/skills/ce-handoff/SKILL.md +1 -1
  187. package/skills/jgengine/SKILL.md +22 -2
  188. package/skills/jgengine/api.md +28 -17
  189. package/skills/jgengine/recipes/minimal-game.md +7 -2
  190. package/skills/jgengine-ui/SKILL.md +3 -1
  191. package/skills/jgengine-ui/api.md +204 -67
  192. package/skills/jgengine-ui/capabilities.md +115 -1
  193. package/skills/jgengine-ui/reference-primitives.md +93 -3
  194. package/skills/jgengine-ui/reference.md +2 -0
  195. package/skills/jgengine-world/SKILL.md +12 -3
  196. package/skills/jgengine-world/api.md +200 -57
  197. package/skills/jgengine-world/capabilities.md +116 -3
  198. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  199. package/skills/jgengine-world/recipes/controls.md +4 -0
  200. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  201. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  202. package/skills/jgengine-world/reference.md +152 -5
  203. package/skills/jgengine-world/visibility.md +28 -1
@@ -1,6 +1,7 @@
1
1
  import type { EditorDocument } from "@jgengine/core/editor/index";
2
2
  import type { ModelConfig } from "@jgengine/core/game/playableGame";
3
3
  import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
4
+ import { type ResolveAuthoredObjectsOptions } from "@jgengine/core/world/authoredObjects";
4
5
  import type { TerrainField } from "@jgengine/core/world/terrain";
5
6
  /**
6
7
  * Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
@@ -23,7 +24,7 @@ export interface AuthoredPathsProps {
23
24
  */
24
25
  export declare function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): import("react").JSX.Element;
25
26
  /** Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. */
26
- export interface AuthoredObjectsProps {
27
+ export interface AuthoredObjectsProps extends ResolveAuthoredObjectsOptions {
27
28
  document: EditorDocument;
28
29
  field: TerrainField;
29
30
  /** Extra lift in meters on top of each marker's own verticalOffset (default 0). */
@@ -33,13 +34,15 @@ export interface AuthoredObjectsProps {
33
34
  * `placeAuthoredObjects` call wins and remounts stay remount-safe.
34
35
  */
35
36
  onExisting?: "throw" | "replace" | "keep";
37
+ /** Synchronize authored edits and remove previously owned placements absent from the document. */
38
+ synchronize?: boolean;
36
39
  }
37
40
  /**
38
41
  * Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
39
42
  * WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
40
43
  * headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
41
44
  */
42
- export declare function AuthoredObjects({ document, field, verticalOffset, onExisting, }: AuthoredObjectsProps): null;
45
+ export declare function AuthoredObjects({ document, field, verticalOffset, onExisting, synchronize, excludeKinds, }: AuthoredObjectsProps): null;
43
46
  /**
44
47
  * Writes the document's studio solids (city buildings and any kind with a `solids` hook) into
45
48
  * `ctx.world.solids`, so what the studios draw also blocks the player, NPCs and physics.
@@ -52,6 +55,8 @@ export declare function AuthoredSolids({ document, field }: {
52
55
  /** Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. */
53
56
  export interface AuthoredSceneProps {
54
57
  document: EditorDocument;
58
+ /** Register bounded procedural weather counts in the supported diagnostics probes. */
59
+ diagnostics?: boolean;
55
60
  field: TerrainField;
56
61
  /** Restrict rendered path kinds; default renders every non-scatter path. */
57
62
  pathKinds?: readonly string[];
@@ -61,6 +66,8 @@ export interface AuthoredSceneProps {
61
66
  * dedicated map even if it shares entries with `entityModels`). Requires `assets`.
62
67
  */
63
68
  scatterModels?: Record<string, string | ModelConfig>;
69
+ /** Animated models keyed by authored cosmetic habitat species. */
70
+ flockModels?: Record<string, string | ModelConfig>;
64
71
  /** Catalog `scatterModels` string ids resolve through; required together with `scatterModels`. */
65
72
  assets?: AssetCatalog;
66
73
  /**
@@ -72,11 +79,12 @@ export interface AuthoredSceneProps {
72
79
  /**
73
80
  * Place the document's catalog-id markers into the object
74
81
  * store — WorldScene renders them via the game's `objectModels` seam. Omit when the game places
75
- * props itself in onInit with `placeAuthoredObjects`.
82
+ * props itself in onInit with `placeAuthoredObjects`. `excludeKinds` replaces the resolver's
83
+ * default mob/boss exclusions; include those when adding other game-owned entity spawn kinds.
76
84
  */
77
- placeObjects?: boolean | {
85
+ placeObjects?: boolean | ({
78
86
  verticalOffset?: number;
79
- };
87
+ } & ResolveAuthoredObjectsOptions);
80
88
  /** Terrain surface color sampler forwarded to studio renderers (see `SceneKindRenderContext.groundColorAt`). */
81
89
  groundColorAt?: (x: number, z: number) => string;
82
90
  }
@@ -90,4 +98,4 @@ export interface AuthoredSceneProps {
90
98
  * place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
91
99
  * real catalog GLBs; unmapped items keep the stylized proxy.
92
100
  */
93
- export declare function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
101
+ export declare function AuthoredScene({ document, diagnostics, field, pathKinds, scatterModels, flockModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
@@ -1,8 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import { AuthoredFlocks } from "../world/AuthoredFlocks.js";
3
+ import { AuthoredWeatherLayer } from "../weather/AuthoredWeatherLayer.js";
4
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
5
  import * as THREE from "three";
4
6
  import { getDocumentLiveSync, subscribeDocumentLiveSyncInstall, } from "@jgengine/core/editor/index";
5
- import { placeAuthoredObjects, resolveAuthoredObjects, } from "@jgengine/core/world/authoredObjects";
7
+ import { placeAuthoredObjects, resolveAuthoredObjectsWithDiagnostics, syncAuthoredObjects, } from "@jgengine/core/world/authoredObjects";
6
8
  import { syncAuthoredSolids } from "@jgengine/core/world/authoredSolids";
7
9
  import { buildRoadRibbon, GROUND_DECAL_LAYERS, roundPathCorners } from "@jgengine/core/world/roads";
8
10
  import { isScatterPath, resolveScatter } from "@jgengine/core/world/scatterRegion";
@@ -131,17 +133,32 @@ export function AuthoredPaths({ document, field, kinds }) {
131
133
  * WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
132
134
  * headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
133
135
  */
134
- export function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", }) {
136
+ export function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", synchronize = false, excludeKinds, }) {
135
137
  const ctx = useGameContext();
136
- const objects = useMemo(() => resolveAuthoredObjects(document), [document]);
138
+ const previousObjects = useRef([]);
139
+ const previousStore = useRef(ctx.scene.object);
140
+ const resolved = useMemo(() => resolveAuthoredObjectsWithDiagnostics(document, excludeKinds === undefined ? {} : { excludeKinds }), [document, excludeKinds]);
141
+ const warnedResult = useRef(null);
142
+ const objects = resolved.objects;
137
143
  useEffect(() => {
138
- if (objects.length === 0)
144
+ if (warnedResult.current !== resolved) {
145
+ warnedResult.current = resolved;
146
+ for (const diagnostic of resolved.diagnostics)
147
+ console.warn(`[jgengine] ${diagnostic.path}: ${diagnostic.message}. ${diagnostic.repair}`);
148
+ }
149
+ if (previousStore.current !== ctx.scene.object) {
150
+ previousObjects.current = [];
151
+ previousStore.current = ctx.scene.object;
152
+ }
153
+ if (synchronize) {
154
+ previousObjects.current = syncAuthoredObjects(ctx.scene.object, objects, previousObjects.current, (x, z) => field.sampleHeight(x, z), { verticalOffset });
139
155
  return;
156
+ }
140
157
  placeAuthoredObjects(ctx.scene.object, objects, (x, z) => field.sampleHeight(x, z), {
141
158
  verticalOffset,
142
159
  onExisting,
143
160
  });
144
- }, [ctx.scene.object, field, objects, onExisting, verticalOffset]);
161
+ }, [ctx.scene.object, field, resolved, objects, onExisting, verticalOffset, synchronize]);
145
162
  return null;
146
163
  }
147
164
  /**
@@ -169,16 +186,20 @@ export function AuthoredSolids({ document, field }) {
169
186
  * place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
170
187
  * real catalog GLBs; unmapped items keep the stylized proxy.
171
188
  */
172
- export function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, groundColorAt, }) {
189
+ export function AuthoredScene({ document, diagnostics = false, field, pathKinds, scatterModels, flockModels, assets, live = true, placeObjects, groundColorAt, }) {
190
+ const ctx = useGameContext();
173
191
  const liveDocument = useLiveEditorDocument(document, live);
192
+ // Authoring updates configuration only. Physics remains owned by the fixed game-time stages.
193
+ useEffect(() => { ctx.environment.retune(liveDocument); }, [ctx.environment, liveDocument]);
174
194
  const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]);
175
195
  const resolveItem = useMemo(() => createModelMapResolver(scatterModels, assets, "scatterModels"), [scatterModels, assets]);
176
196
  const shouldPlaceObjects = placeObjects === true || (typeof placeObjects === "object" && placeObjects !== null);
177
197
  const objectVerticalOffset = typeof placeObjects === "object" && placeObjects !== null ? (placeObjects.verticalOffset ?? 0) : 0;
178
- return (_jsxs(_Fragment, { children: [_jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
198
+ const objectExcludeKinds = typeof placeObjects === "object" && placeObjects !== null ? placeObjects.excludeKinds : undefined;
199
+ return (_jsxs(_Fragment, { children: [_jsx(AuthoredWeatherLayer, { document: liveDocument, heightAt: field.sampleHeight, diagnostics: diagnostics }), flockModels === undefined ? null : _jsx(AuthoredFlocks, { models: flockModels, assets: assets }), _jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
179
200
  document: liveDocument,
180
201
  field,
181
202
  ...(assets === undefined ? {} : { assets }),
182
203
  ...(groundColorAt === undefined ? {} : { groundColorAt }),
183
- } }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), _jsx(AuthoredSolids, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset })) : null] }));
204
+ } }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), _jsx(AuthoredSolids, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset, synchronize: live && getDocumentLiveSync() !== null, ...(objectExcludeKinds === undefined ? {} : { excludeKinds: objectExcludeKinds }) })) : null] }));
184
205
  }
@@ -1,49 +1,18 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef } from "react";
3
- import * as THREE from "three";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
4
3
  import { resolveGeneratorAsset } from "@jgengine/core/scene/assetGenerator";
5
- const UNIT_BOX = new THREE.BoxGeometry(1, 1, 1);
4
+ import { StaticShapeInstances } from "../render/staticShapeInstances.js";
6
5
  const NEUTRAL = "#b8b0a4";
7
6
  /** All parts of one generated asset, batched into one instanced box draw per color. */
8
7
  function GeneratedParts({ asset }) {
9
- const byColor = useMemo(() => {
10
- const groups = new Map();
11
- for (const part of asset.parts) {
12
- const color = part.color ?? NEUTRAL;
13
- const bucket = groups.get(color);
14
- if (bucket === undefined)
15
- groups.set(color, [part]);
16
- else
17
- bucket.push(part);
18
- }
19
- return [...groups.entries()];
20
- }, [asset]);
21
- return (_jsx(_Fragment, { children: byColor.map(([color, parts]) => (_jsx(ColorBatch, { color: color, parts: parts }, color))) }));
22
- }
23
- function ColorBatch({ color, parts }) {
24
- const meshRef = useRef(null);
25
- useEffect(() => {
26
- const mesh = meshRef.current;
27
- if (mesh === null)
28
- return;
29
- const matrix = new THREE.Matrix4();
30
- const position = new THREE.Vector3();
31
- const quaternion = new THREE.Quaternion();
32
- const euler = new THREE.Euler();
33
- const scale = new THREE.Vector3();
34
- parts.forEach((part, index) => {
35
- position.set(part.position[0], part.position[1], part.position[2]);
36
- euler.set(0, part.rotationY ?? 0, 0);
37
- quaternion.setFromEuler(euler);
38
- scale.set(Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2]));
39
- matrix.compose(position, quaternion, scale);
40
- mesh.setMatrixAt(index, matrix);
41
- });
42
- mesh.count = parts.length;
43
- mesh.instanceMatrix.needsUpdate = true;
44
- mesh.computeBoundingSphere();
45
- }, [parts]);
46
- return (_jsx("instancedMesh", { ref: meshRef, args: [UNIT_BOX, undefined, Math.max(1, parts.length)], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 0.62, metalness: 0.04 }) }));
8
+ const instances = useMemo(() => asset.parts.map(part => ({
9
+ shape: "box",
10
+ position: part.position,
11
+ rotation: [0, part.rotationY ?? 0, 0],
12
+ scale: [Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2])],
13
+ surface: { color: part.color ?? NEUTRAL, roughness: 0.62, metalness: 0.04 },
14
+ })), [asset]);
15
+ return _jsx(StaticShapeInstances, { instances: instances });
47
16
  }
48
17
  /**
49
18
  * Renders one placed generator-asset instance by re-resolving its `meta` through the registered
@@ -1,5 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
+ import { ScrollRail } from "@jgengine/react/scrollRail";
4
+ import { useDialogBehavior } from "@jgengine/react/dialogBehavior";
3
5
  import { useDebouncedCommit } from "@jgengine/react/useDebouncedCommit";
4
6
  import {} from "./settingsController.js";
5
7
  const surface = "var(--jg-surface, #16161d)";
@@ -40,9 +42,9 @@ function ToggleRow({ row }) {
40
42
  function SelectRow({ row }) {
41
43
  const options = row.options ?? [];
42
44
  const current = String(row.value);
43
- return (_jsx(RowShell, { label: row.label, control: _jsx("div", { className: "flex flex-wrap justify-end gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
45
+ return (_jsx(RowShell, { label: row.label, control: _jsx("div", { role: "group", "aria-label": row.label, className: "flex flex-wrap justify-end gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
44
46
  const selected = option.value === current;
45
- return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: "min-h-[34px] rounded-md px-3 text-sm font-medium transition", style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: option.label }, option.value));
47
+ return (_jsx("button", { type: "button", onClick: () => row.set(option.value), "aria-pressed": selected, className: "min-h-[34px] rounded-md px-3 text-sm font-medium transition", style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: option.label }, option.value));
46
48
  }) }) }));
47
49
  }
48
50
  function RowControl({ row }) {
@@ -68,10 +70,10 @@ function ActionRow({ action, onClose }) {
68
70
  }, children: [_jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-sm font-semibold", style: { color: isDanger ? danger : text }, children: action.label }), action.description !== undefined ? (_jsx("span", { className: "text-xs", style: { color: textDim }, children: action.description })) : null] }), _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4 shrink-0", style: { color: isDanger ? danger : textDim }, "aria-hidden": true, children: _jsx("path", { d: "M9 6l6 6-6 6" }) })] }));
69
71
  }
70
72
  function TabButton({ tab, selected, orientation, onSelect }) {
71
- return (_jsx("button", { type: "button", onClick: onSelect, className: `min-h-[40px] whitespace-nowrap rounded-lg px-4 text-sm font-medium transition ${orientation === "col" ? "w-full text-left" : ""} ${selected ? "" : "hover:bg-white/5"}`, style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: tab.label }));
73
+ return (_jsx("button", { type: "button", onClick: onSelect, className: `shrink-0 min-h-[44px] whitespace-nowrap rounded-lg px-4 text-sm font-medium transition ${orientation === "col" ? "w-full text-left" : ""} ${selected ? "" : "hover:bg-white/5"}`, style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: tab.label }));
72
74
  }
73
75
  function Body({ tab, actions, capturing, setCapturing, onClose }) {
74
- return (_jsx("div", { className: "jg-settings-scroll flex-1 space-y-2 overflow-y-auto p-4", children: tab.category === null
76
+ return (_jsx("div", { className: "jg-settings-scroll min-h-0 flex-1 space-y-2 overflow-y-auto p-4", children: tab.category === null
75
77
  ? actions.map((action) => _jsx(ActionRow, { action: action, onClose: onClose }, action.id))
76
78
  : (_jsxs(_Fragment, { children: [tab.category.rows.map((row) => (_jsx(RowControl, { row: row }, row.id))), tab.category.keybinds.map((row) => (_jsx(KeybindRowView, { row: row, capturing: capturing?.action === row.action, onCapture: () => setCapturing(row) }, row.action)))] })) }));
77
79
  }
@@ -95,6 +97,7 @@ export function SettingsMenu({ controller, onClose, initialTab, }) {
95
97
  const [activeId, setActiveId] = useState(initialTab !== undefined && tabs.some((tab) => tab.id === initialTab) ? initialTab : (tabs[0]?.id ?? ""));
96
98
  const [capturing, setCapturing] = useState(null);
97
99
  const active = tabs.find((tab) => tab.id === activeId) ?? tabs[0] ?? null;
100
+ const dialogRef = useDialogBehavior({ open: active !== null, onClose: capturing === null ? onClose : undefined });
98
101
  useEffect(() => {
99
102
  if (capturing === null)
100
103
  return;
@@ -124,28 +127,18 @@ export function SettingsMenu({ controller, onClose, initialTab, }) {
124
127
  window.removeEventListener("contextmenu", onContext, true);
125
128
  };
126
129
  }, [capturing]);
127
- useEffect(() => {
128
- if (capturing !== null)
129
- return;
130
- const onKey = (event) => {
131
- if (event.code === "Escape")
132
- onClose();
133
- };
134
- window.addEventListener("keydown", onKey);
135
- return () => window.removeEventListener("keydown", onKey);
136
- }, [capturing, onClose]);
137
130
  if (active === null)
138
131
  return null;
139
132
  const bodyProps = { tab: active, actions: controller.actions, capturing, setCapturing, onClose };
140
133
  const title = (_jsx("h2", { className: "text-base font-semibold tracking-wide", style: { color: text }, children: "Settings" }));
141
134
  if (controller.variant === "fullscreen") {
142
- return (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-40 flex flex-col", style: { background: surface, color: text, fontFamily: fontDisplay }, children: _jsxs("div", { className: "mx-auto flex h-full w-full max-w-2xl flex-col px-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 pb-4 pt-6", children: [_jsx("h2", { className: "text-2xl font-bold tracking-wide", style: { color: text }, children: "Settings" }), _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto pb-4", children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
135
+ return (_jsx("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, className: "pointer-events-auto absolute inset-0 z-40 flex flex-col", style: { background: surface, color: text, fontFamily: fontDisplay }, children: _jsxs("div", { className: "mx-auto flex h-full w-full max-w-2xl flex-col px-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 pb-4 pt-6", children: [_jsx("h2", { className: "text-2xl font-bold tracking-wide", style: { color: text }, children: "Settings" }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(ScrollRail, { label: "settings categories", style: { paddingBottom: 16 }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
143
136
  }
144
137
  if (controller.variant === "sidebar") {
145
- return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(94vw, 760px)", height: "min(84vh, 560px)" }, children: [_jsxs("nav", { className: "flex w-44 shrink-0 flex-col gap-1 p-3", style: { background: surfaceDeep, borderRight: `1px solid ${edge}` }, children: [_jsx("span", { className: "px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-widest", style: { color: textDim }, children: "Settings" }), tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "col", onSelect: () => setActiveId(tab.id) }, tab.id)))] }), _jsxs("div", { className: "flex flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: [_jsx("span", { className: "text-sm font-semibold", style: { color: text }, children: active.label }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(Body, { ...bodyProps })] })] }) }));
138
+ return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, onClick: stop, className: "flex overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(94vw, 760px)", height: "min(84vh, 560px)" }, children: [_jsxs("nav", { className: "flex w-44 shrink-0 flex-col gap-1 overflow-y-auto p-3", style: { background: surfaceDeep, borderRight: `1px solid ${edge}` }, children: [_jsx("span", { className: "px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-widest", style: { color: textDim }, children: "Settings" }), tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "col", onSelect: () => setActiveId(tab.id) }, tab.id)))] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: [_jsx("span", { className: "text-sm font-semibold", style: { color: text }, children: active.label }), _jsx(CloseButton, { onClose: onClose })] }), _jsx(Body, { ...bodyProps })] })] }) }));
146
139
  }
147
140
  if (controller.variant === "sheet") {
148
- return (_jsx(Backdrop, { onClose: onClose, align: "items-end", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-t-3xl shadow-2xl", style: { ...panelStyle, width: "min(100%, 640px)", height: "min(84vh, 620px)" }, children: [_jsx("div", { className: "flex flex-col items-center pt-2.5", children: _jsx("span", { className: "h-1.5 w-10 rounded-full", style: { background: "rgba(255,255,255,0.25)" } }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto px-4 pb-3", children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx("div", { style: { borderTop: `1px solid ${edge}` }, className: "flex flex-1 flex-col overflow-hidden", children: _jsx(Body, { ...bodyProps }) })] }) }));
141
+ return (_jsx(Backdrop, { onClose: onClose, align: "items-end", children: _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, onClick: stop, className: "flex flex-col overflow-hidden rounded-t-3xl shadow-2xl", style: { ...panelStyle, width: "min(100%, 640px)", height: "min(84vh, 620px)" }, children: [_jsx("div", { className: "flex flex-col items-center pt-2.5", children: _jsx("span", { className: "h-1.5 w-10 rounded-full", style: { background: "rgba(255,255,255,0.25)" } }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx(ScrollRail, { label: "settings categories", style: { padding: "0 16px 12px" }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx("div", { style: { borderTop: `1px solid ${edge}` }, className: "flex flex-1 flex-col overflow-hidden", children: _jsx(Body, { ...bodyProps }) })] }) }));
149
142
  }
150
- return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(92vw, 460px)", height: "min(86vh, 580px)" }, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3.5", style: { borderBottom: `1px solid ${edge}` }, children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
143
+ return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": "Settings", tabIndex: -1, onClick: stop, className: "flex flex-col overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(92vw, 460px)", height: "min(86vh, 580px)" }, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3.5", style: { borderBottom: `1px solid ${edge}` }, children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx(ScrollRail, { label: "settings categories", style: { padding: "12px 16px", borderBottom: `1px solid ${edge}` }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
151
144
  }
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
3
  import type { SettingsSurface, SettingsVariant } from "@jgengine/core/settings/settingsModel";
3
4
  import { type SettingsActionView } from "@jgengine/react/settings";
4
5
  import { type SettingsControllerInput } from "./settingsController.js";
@@ -9,3 +10,7 @@ export interface SettingsRuntimeProps extends SettingsControllerInput {
9
10
  children: ReactNode;
10
11
  }
11
12
  export declare function SettingsRuntime({ variant, surface, actions, children, ...input }: SettingsRuntimeProps): import("react").JSX.Element;
13
+ /** Owns settings' suspension for this live context, including close, replacement and unmount. @internal */
14
+ export declare function SettingsPlayControlGate({ ctx }: {
15
+ ctx: GameContext;
16
+ }): import("react").JSX.Element;
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
3
- import { SettingsControllerProvider, } from "@jgengine/react/settings";
2
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { suspendPlayControls } from "@jgengine/core/game/controlGate";
4
+ import { SettingsControllerProvider, useSettings, } from "@jgengine/react/settings";
5
+ import { createPlaySurfaceFocusOwnership } from "../presentationRecovery.js";
4
6
  import { useSettingsCategories } from "./settingsController.js";
5
7
  export function SettingsRuntime({ variant, surface, actions, children, ...input }) {
6
8
  const categories = useSettingsCategories(input);
@@ -17,3 +19,28 @@ export function SettingsRuntime({ variant, surface, actions, children, ...input
17
19
  }), [categories, actions, variant, surface, isOpen]);
18
20
  return _jsx(SettingsControllerProvider, { controller: controller, children: children });
19
21
  }
22
+ /** Owns settings' suspension for this live context, including close, replacement and unmount. @internal */
23
+ export function SettingsPlayControlGate({ ctx }) {
24
+ const { isOpen } = useSettings();
25
+ const anchor = useRef(null);
26
+ const openedSurface = useRef(null);
27
+ const focus = useRef(createPlaySurfaceFocusOwnership());
28
+ useLayoutEffect(() => {
29
+ if (isOpen) {
30
+ openedSurface.current = { owner: ctx, surface: anchor.current?.closest('div[tabindex="0"]') ?? null };
31
+ return;
32
+ }
33
+ const previous = openedSurface.current;
34
+ openedSurface.current = null;
35
+ if (previous?.owner !== ctx)
36
+ return;
37
+ focus.current.request(ctx);
38
+ focus.current.reveal(ctx, previous.surface, document.activeElement, document.body);
39
+ }, [ctx, isOpen]);
40
+ useLayoutEffect(() => {
41
+ if (!isOpen)
42
+ return;
43
+ return suspendPlayControls(ctx);
44
+ }, [ctx, isOpen]);
45
+ return _jsx("span", { hidden: true, ref: anchor });
46
+ }
@@ -0,0 +1,18 @@
1
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
+ import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
+ type Ref<T> = {
4
+ current: T;
5
+ };
6
+ /** Clears held input synchronously on play-gate transitions; ordinary context swaps retain it. @internal */
7
+ export declare function attachShellControlSuspension({ ctx, tracker, pointerAxisRef, analogRef, primaryClickRef, cameraDraggingRef, f2HeldRef, }: {
8
+ ctx: GameContext;
9
+ tracker: {
10
+ reset(): void;
11
+ };
12
+ pointerAxisRef: Ref<PointerAxisState | null>;
13
+ analogRef: Ref<Readonly<Record<string, number>> | null>;
14
+ primaryClickRef: Ref<boolean>;
15
+ cameraDraggingRef: Ref<boolean>;
16
+ f2HeldRef: Ref<boolean>;
17
+ }): () => void;
18
+ export {};
@@ -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;