@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.
- package/CHANGELOG.md +346 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +12 -5
- package/dist/defineGame.js +24 -9
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +3 -0
- package/dist/render/modelLoad.js +9 -0
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +21 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +209 -61
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +22 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +204 -67
- package/skills/jgengine-ui/capabilities.md +115 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +200 -57
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +152 -5
- 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 {
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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-[
|
|
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(
|
|
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("
|
|
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(
|
|
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 {
|
|
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;
|