@jgengine/shell 0.10.0 → 0.11.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 +13 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayerShell.d.ts +0 -12
- package/dist/GamePlayerShell.js +88 -1043
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.js +30 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +2 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +94 -24
- package/dist/camera/rigMath.js +100 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/cartridge.d.ts +1 -1
- package/dist/cartridge.js +3 -3
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +25 -6
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
- package/dist/devtools/DevtoolsOverlay.js +19 -450
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +262 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +32 -6
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.js +47 -9
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/registry.d.ts +3 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +73 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +39 -3
- package/dist/render/resolveModel.js +79 -2
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +130 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +94 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +82 -0
- package/dist/scene/AuthoredScene.js +173 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +25 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +21 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +37 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +22 -0
- package/dist/scene/sceneKindRenderers.d.ts +28 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +5 -0
- package/dist/settings/appliedSettings.js +9 -0
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +43 -0
- package/dist/structures/TransformGizmo.js +55 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.js +1 -1
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +3 -0
- package/dist/terrain/grassMaterial.d.ts +2 -0
- package/dist/terrain/grassMaterial.js +2 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +10 -0
- package/dist/terrain/soilPatchMaterial.js +109 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +72 -5
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/OceanConfig.d.ts +2 -0
- package/dist/water/OceanConfig.js +2 -0
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +159 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +38 -0
- package/skills/jgengine-world/api.md +2652 -0
- package/skills/jgengine-world/capabilities.md +370 -0
- package/skills/jgengine-world/reference.md +222 -0
- package/llms.txt +0 -2761
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { resolveGrassObject } from "@jgengine/core/world/grassKind";
|
|
4
|
+
import { GrassField } from "../terrain/GrassField.js";
|
|
5
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
6
|
+
function hashSeed(text) {
|
|
7
|
+
let h = 2166136261 >>> 0;
|
|
8
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
9
|
+
h ^= text.charCodeAt(i);
|
|
10
|
+
h = Math.imul(h, 16777619);
|
|
11
|
+
}
|
|
12
|
+
return (h >>> 0) % 100000;
|
|
13
|
+
}
|
|
14
|
+
/** One authored grass volume → an instanced vertex-wind `GrassField` over its footprint. */
|
|
15
|
+
function OneGrass({ object, context }) {
|
|
16
|
+
const resolved = useMemo(() => resolveGrassObject(object), [object]);
|
|
17
|
+
if (resolved === null)
|
|
18
|
+
return null;
|
|
19
|
+
const { rules, size, center } = resolved;
|
|
20
|
+
return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: [size[0], size[1]], density: rules.density, bladeHeight: [rules.bladeHeight * 0.7, rules.bladeHeight], colorBase: rules.colorBase, colorTip: rules.colorTip, colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: (x, z) => context.field.sampleHeight(x, z), wind: { strength: rules.windStrength, speed: rules.windSpeed, gustScale: rules.windGust } }));
|
|
21
|
+
}
|
|
22
|
+
/** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
23
|
+
export function registerGrassFieldRenderer() {
|
|
24
|
+
registerSceneKindRenderer("grass_field", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneGrass, { object: object, context: context }, object.id))) })));
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { resolveSoilObject } from "@jgengine/core/world/soilKind";
|
|
4
|
+
import { createSoilPatchMaterial } from "../terrain/soilPatchMaterial.js";
|
|
5
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
6
|
+
const SOIL_PATCH_THICKNESS = 0.08;
|
|
7
|
+
/** One authored soil volume → a ground-hugging patch mesh with a crack/moss shader material. */
|
|
8
|
+
function OneSoil({ object, context }) {
|
|
9
|
+
const resolved = useMemo(() => resolveSoilObject(object), [object]);
|
|
10
|
+
const material = useMemo(() => (resolved === null ? null : createSoilPatchMaterial(resolved.rules)), [resolved]);
|
|
11
|
+
useEffect(() => () => material?.dispose(), [material]);
|
|
12
|
+
if (resolved === null || material === null)
|
|
13
|
+
return null;
|
|
14
|
+
const groundY = context.field.sampleHeight(resolved.center[0], resolved.center[2]);
|
|
15
|
+
const y = groundY + SOIL_PATCH_THICKNESS / 2 + 0.02;
|
|
16
|
+
return (_jsx("mesh", { position: [resolved.center[0], y, resolved.center[2]], material: material, receiveShadow: true, children: _jsx("boxGeometry", { args: [resolved.size[0], SOIL_PATCH_THICKNESS, resolved.size[1]] }) }));
|
|
17
|
+
}
|
|
18
|
+
/** Registers the `soil` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
19
|
+
export function registerSoilPatchRenderer() {
|
|
20
|
+
registerSceneKindRenderer("soil", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneSoil, { object: object, context: context }, object.id))) })));
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Lighting mood for a {@link StudioStage} — a named 3-point rig + backdrop palette. */
|
|
3
|
+
export type StudioMood = "studio" | "daylight" | "dusk" | "night";
|
|
4
|
+
/** Props for {@link StudioStage}. */
|
|
5
|
+
export interface StudioStageProps {
|
|
6
|
+
mood?: StudioMood;
|
|
7
|
+
/** Override the scene background color (hex). Default: the mood's backdrop. */
|
|
8
|
+
backdrop?: string;
|
|
9
|
+
/** Turntable spin speed in radians/second (spins the children). 0 disables. Default 0. */
|
|
10
|
+
turntable?: number;
|
|
11
|
+
/** Draw the seamless floor + set the scene background. Off for open-world scenes. Default true. */
|
|
12
|
+
environment?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Yaw the children every frame so `forward` points at the active camera — frame the declared front
|
|
15
|
+
* with zero `rotationY` tuning. Overrides `turntable` (the two are mutually exclusive: one spins the
|
|
16
|
+
* content, the other holds it facing whoever's looking). Default false.
|
|
17
|
+
*/
|
|
18
|
+
faceCamera?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The staged content's declared forward axis (`GeneratedAsset.forward`, or a scene kind's own
|
|
21
|
+
* convention) — the direction `faceCamera` turns to point at the camera. Default `[0, 0, 1]`, the
|
|
22
|
+
* engine-wide convention (`DEFAULT_FORWARD` in `@jgengine/core/scene/facing`).
|
|
23
|
+
*/
|
|
24
|
+
forward?: readonly [number, number, number];
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A reusable cinematic "product-shot" stage — a 3-point lighting rig (key/fill/rim + ambient), a
|
|
29
|
+
* seamless backdrop, and an optional turntable — so any parametric studio renders framed and lit like
|
|
30
|
+
* a hero shot instead of a flat proxy under default light. Pair with `PlayableGame.postProcessing =
|
|
31
|
+
* STUDIO_STAGE_POST` for the full film grade. Set `environment: false` to keep an open-world sky/ground
|
|
32
|
+
* and use it purely as a lighting rig. Set `faceCamera` to auto-orient the declared `forward` toward
|
|
33
|
+
* the camera instead of hand-tuning `rotationY` on the child.
|
|
34
|
+
*
|
|
35
|
+
* @capability studio-stage cinematic lighting rig + backdrop + turntable for parametric studios
|
|
36
|
+
*/
|
|
37
|
+
export declare function StudioStage({ mood, backdrop, turntable, environment, faceCamera, forward, children, }: StudioStageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { DEFAULT_FORWARD, yawToFace } from "@jgengine/core/scene/facing";
|
|
6
|
+
const MOODS = {
|
|
7
|
+
studio: { key: 3.1, keyColor: "#fff4e6", fill: 0.9, fillColor: "#cdd8ff", rim: 2.6, rimColor: "#ffe9c7", ambient: 0.35, ambientColor: "#5b6472", backdrop: "#14161c", floor: "#1b1e26" },
|
|
8
|
+
daylight: { key: 3.4, keyColor: "#fff6ea", fill: 1.2, fillColor: "#d6e6ff", rim: 1.6, rimColor: "#ffffff", ambient: 0.6, ambientColor: "#8fa2bf", backdrop: "#cdd7e4", floor: "#aeb7c4" },
|
|
9
|
+
dusk: { key: 2.6, keyColor: "#ffcf9e", fill: 0.7, fillColor: "#7c8fd8", rim: 3.0, rimColor: "#ff9d6b", ambient: 0.4, ambientColor: "#4a4668", backdrop: "#221a26", floor: "#241d2a" },
|
|
10
|
+
night: { key: 1.6, keyColor: "#bcd0ff", fill: 0.4, fillColor: "#38507e", rim: 2.4, rimColor: "#9fd0ff", ambient: 0.22, ambientColor: "#1e2740", backdrop: "#0a0c12", floor: "#0e1119" },
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A reusable cinematic "product-shot" stage — a 3-point lighting rig (key/fill/rim + ambient), a
|
|
14
|
+
* seamless backdrop, and an optional turntable — so any parametric studio renders framed and lit like
|
|
15
|
+
* a hero shot instead of a flat proxy under default light. Pair with `PlayableGame.postProcessing =
|
|
16
|
+
* STUDIO_STAGE_POST` for the full film grade. Set `environment: false` to keep an open-world sky/ground
|
|
17
|
+
* and use it purely as a lighting rig. Set `faceCamera` to auto-orient the declared `forward` toward
|
|
18
|
+
* the camera instead of hand-tuning `rotationY` on the child.
|
|
19
|
+
*
|
|
20
|
+
* @capability studio-stage cinematic lighting rig + backdrop + turntable for parametric studios
|
|
21
|
+
*/
|
|
22
|
+
export function StudioStage({ mood = "studio", backdrop, turntable = 0, environment = true, faceCamera = false, forward = DEFAULT_FORWARD, children, }) {
|
|
23
|
+
const spin = useRef(null);
|
|
24
|
+
const camera = useThree((state) => state.camera);
|
|
25
|
+
const groupWorld = useRef(new THREE.Vector3()).current;
|
|
26
|
+
const cameraWorld = useRef(new THREE.Vector3()).current;
|
|
27
|
+
useFrame((_, delta) => {
|
|
28
|
+
const group = spin.current;
|
|
29
|
+
if (group === null)
|
|
30
|
+
return;
|
|
31
|
+
if (faceCamera) {
|
|
32
|
+
group.getWorldPosition(groupWorld);
|
|
33
|
+
camera.getWorldPosition(cameraWorld);
|
|
34
|
+
group.rotation.y = yawToFace(forward, [groupWorld.x, groupWorld.z], [cameraWorld.x, cameraWorld.z]);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (turntable !== 0)
|
|
38
|
+
group.rotation.y += turntable * delta;
|
|
39
|
+
});
|
|
40
|
+
const rig = MOODS[mood];
|
|
41
|
+
return (_jsxs(_Fragment, { children: [environment ? _jsx("color", { attach: "background", args: [backdrop ?? rig.backdrop] }) : null, _jsx("ambientLight", { intensity: rig.ambient, color: rig.ambientColor }), _jsx("directionalLight", { position: [7, 9, 5], intensity: rig.key, color: rig.keyColor, castShadow: true, "shadow-mapSize": [2048, 2048], "shadow-bias": -0.0004, children: _jsx("orthographicCamera", { attach: "shadow-camera", args: [-24, 24, 24, -24, 0.5, 80] }) }), _jsx("directionalLight", { position: [-8, 5, 3], intensity: rig.fill, color: rig.fillColor }), _jsx("directionalLight", { position: [0, 6, -10], intensity: rig.rim, color: rig.rimColor }), environment ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": -0.01, receiveShadow: true, children: [_jsx("planeGeometry", { args: [400, 400] }), _jsx("meshStandardMaterial", { color: rig.floor, roughness: 0.95, metalness: 0 })] })) : null, _jsx("group", { ref: spin, children: children })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { resolveWaterObject } from "@jgengine/core/world/waterKind";
|
|
4
|
+
import { Ocean } from "../water/Ocean.js";
|
|
5
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
6
|
+
/** Map the editor-authored water params onto the shell Ocean shader config. */
|
|
7
|
+
function toOceanConfig(rules, size) {
|
|
8
|
+
const maxSpan = Math.max(size[0], size[1]);
|
|
9
|
+
return {
|
|
10
|
+
size: size[0],
|
|
11
|
+
depth: size[1],
|
|
12
|
+
resolution: Math.min(160, Math.max(16, Math.round(maxSpan / 2))),
|
|
13
|
+
amplitude: rules.waveHeight,
|
|
14
|
+
speed: rules.waveSpeed,
|
|
15
|
+
timeScale: rules.waveSpeed,
|
|
16
|
+
waveScale: Math.min(300, Math.max(2, 6 / rules.waveScale)),
|
|
17
|
+
color: {
|
|
18
|
+
deep: rules.color,
|
|
19
|
+
shallow: rules.shallowColor,
|
|
20
|
+
opacity: rules.opacity,
|
|
21
|
+
fresnelStrength: rules.reflectivity,
|
|
22
|
+
},
|
|
23
|
+
foam: { coverage: Math.min(1, rules.foam / 4), intensity: Math.min(1.5, rules.foam / 3) },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** One authored water volume → a bounded, animated Ocean plane at the box top. */
|
|
27
|
+
function OneWater({ object }) {
|
|
28
|
+
const resolved = useMemo(() => resolveWaterObject(object), [object]);
|
|
29
|
+
const config = useMemo(() => (resolved === null ? null : toOceanConfig(resolved.rules, resolved.size)), [resolved]);
|
|
30
|
+
if (resolved === null || config === null)
|
|
31
|
+
return null;
|
|
32
|
+
return _jsx(Ocean, { config: config, position: [resolved.center[0], resolved.center[1], resolved.center[2]] });
|
|
33
|
+
}
|
|
34
|
+
/** Registers the `water` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
35
|
+
export function registerWaterRenderer() {
|
|
36
|
+
registerSceneKindRenderer("water", ({ objects }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneWater, { object: object }, object.id))) })));
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the engine's built-in *environment* scene-kind renderers (the shell half of #809) and
|
|
3
|
+
* ensures the matching core kinds are registered too. Idempotent; `AuthoredScene` calls it at module
|
|
4
|
+
* load so a document with a `water`/`grass_field`/`soil` volume renders without the game wiring
|
|
5
|
+
* anything up. Domain studios (poles, …) register their own renderer via the public
|
|
6
|
+
* `registerSceneKindRenderer` — no edit here.
|
|
7
|
+
*/
|
|
8
|
+
import { registerBuiltinSceneKinds } from "@jgengine/core/scene/builtinSceneKinds";
|
|
9
|
+
import { registerGrassFieldRenderer } from "./GrassFieldRenderer.js";
|
|
10
|
+
import { registerWaterRenderer } from "./WaterRenderer.js";
|
|
11
|
+
import { registerSoilPatchRenderer } from "./SoilPatchRenderer.js";
|
|
12
|
+
let registered = false;
|
|
13
|
+
/** Registers the engine's built-in environment scene-kind renderers. Called by `AuthoredScene`. @internal */
|
|
14
|
+
export function registerBuiltinSceneKindRenderers() {
|
|
15
|
+
if (registered)
|
|
16
|
+
return;
|
|
17
|
+
registered = true;
|
|
18
|
+
registerBuiltinSceneKinds();
|
|
19
|
+
registerWaterRenderer();
|
|
20
|
+
registerGrassFieldRenderer();
|
|
21
|
+
registerSoilPatchRenderer();
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { EditorDocument } from "@jgengine/core/editor/index";
|
|
3
|
+
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
4
|
+
import type { SceneKindObject } from "@jgengine/core/scene/sceneKinds";
|
|
5
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
6
|
+
/** Context a scene-kind renderer receives alongside the matching document objects. */
|
|
7
|
+
export interface SceneKindRenderContext {
|
|
8
|
+
document: EditorDocument;
|
|
9
|
+
/** Ground field to drape/ground kind geometry on. */
|
|
10
|
+
field: TerrainField;
|
|
11
|
+
/** Catalog for kinds that instance real GLBs (pole assets, etc.). */
|
|
12
|
+
assets?: AssetCatalog;
|
|
13
|
+
}
|
|
14
|
+
/** A registered renderer: given every document object of its kind, returns the render tree for them. */
|
|
15
|
+
export type SceneKindRenderer = (props: {
|
|
16
|
+
objects: readonly SceneKindObject[];
|
|
17
|
+
context: SceneKindRenderContext;
|
|
18
|
+
}) => ReactElement | null;
|
|
19
|
+
/**
|
|
20
|
+
* Register the runtime renderer for a scene kind — the shell half of the #809 seam. `AuthoredScene`
|
|
21
|
+
* groups a document's objects by kind and mounts the registered renderer for each, so a new studio
|
|
22
|
+
* renders from the document with no fork of `AuthoredScene`. Idempotent per kind (last wins).
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerSceneKindRenderer(kind: string, renderer: SceneKindRenderer): void;
|
|
25
|
+
/** The renderer registered for a kind, or undefined. @internal — `AuthoredScene` uses it internally. */
|
|
26
|
+
export declare function getSceneKindRenderer(kind: string): SceneKindRenderer | undefined;
|
|
27
|
+
/** Every kind that has a registered renderer. @internal */
|
|
28
|
+
export declare function renderedSceneKinds(): string[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const renderers = new Map();
|
|
2
|
+
/**
|
|
3
|
+
* Register the runtime renderer for a scene kind — the shell half of the #809 seam. `AuthoredScene`
|
|
4
|
+
* groups a document's objects by kind and mounts the registered renderer for each, so a new studio
|
|
5
|
+
* renders from the document with no fork of `AuthoredScene`. Idempotent per kind (last wins).
|
|
6
|
+
*/
|
|
7
|
+
export function registerSceneKindRenderer(kind, renderer) {
|
|
8
|
+
renderers.set(kind, renderer);
|
|
9
|
+
}
|
|
10
|
+
/** The renderer registered for a kind, or undefined. @internal — `AuthoredScene` uses it internally. */
|
|
11
|
+
export function getSceneKindRenderer(kind) {
|
|
12
|
+
return renderers.get(kind);
|
|
13
|
+
}
|
|
14
|
+
/** Every kind that has a registered renderer. @internal */
|
|
15
|
+
export function renderedSceneKinds() {
|
|
16
|
+
return [...renderers.keys()];
|
|
17
|
+
}
|
package/dist/scene.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./scene/AuthoredScene.js";
|
package/dist/scene.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./scene/AuthoredScene.js";
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
2
|
import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
3
4
|
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
5
|
+
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
6
|
+
export declare const TOUCH_STYLE_AUTO = "auto";
|
|
7
|
+
/** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
|
|
8
|
+
export declare function useTouchStyle(store: SettingsStore, fallback: TouchStyle): TouchStyle;
|
|
4
9
|
export declare function useSettingsRevision(store: SettingsStore): number;
|
|
5
10
|
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
|
|
6
11
|
shadows: boolean;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { useEffect, useReducer } from "react";
|
|
2
2
|
import { busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_DPR, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { TOUCH_STYLES } from "@jgengine/core/input/touchScheme";
|
|
4
|
+
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
5
|
+
export const TOUCH_STYLE_AUTO = "auto";
|
|
6
|
+
/** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
|
|
7
|
+
export function useTouchStyle(store, fallback) {
|
|
8
|
+
useSettingsRevision(store);
|
|
9
|
+
const raw = store.get(SETTING_IDS.touchStyle, TOUCH_STYLE_AUTO);
|
|
10
|
+
return TOUCH_STYLES.includes(raw) ? raw : fallback;
|
|
11
|
+
}
|
|
3
12
|
export function useSettingsRevision(store) {
|
|
4
13
|
const [rev, bump] = useReducer((n) => n + 1, 0);
|
|
5
14
|
useEffect(() => store.subscribe(() => bump()), [store]);
|
|
@@ -12,6 +12,8 @@ export interface SettingsControllerInput {
|
|
|
12
12
|
hide: readonly SettingCategory[];
|
|
13
13
|
fovEnabled: boolean;
|
|
14
14
|
hideBindings: readonly string[];
|
|
15
|
+
/** Show the touch-controls skin selector in Controls (touch device with a dock). */
|
|
16
|
+
touchStyle: boolean;
|
|
15
17
|
overrides: BindingOverrides;
|
|
16
18
|
rebind: (action: string, code: string) => void;
|
|
17
19
|
resetBinding: (action: string) => void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useReducer } from "react";
|
|
2
2
|
import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
3
3
|
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_GRAPHICS_SHADOWS, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
4
|
+
import { TOUCH_STYLE_OPTIONS } from "@jgengine/core/input/touchScheme";
|
|
5
|
+
import { TOUCH_STYLE_AUTO } from "./appliedSettings.js";
|
|
4
6
|
import { useSettingsStore, } from "@jgengine/react/settings";
|
|
5
7
|
import { usePlayerFov } from "../camera/PlayerFov.js";
|
|
6
8
|
const DEFAULT_CATEGORY_LABELS = {
|
|
@@ -10,6 +12,10 @@ const DEFAULT_CATEGORY_LABELS = {
|
|
|
10
12
|
controls: "Controls",
|
|
11
13
|
};
|
|
12
14
|
const percent = (value) => `${Math.round(value * 100)}%`;
|
|
15
|
+
const TOUCH_STYLE_SELECT_OPTIONS = [
|
|
16
|
+
{ value: TOUCH_STYLE_AUTO, label: "Auto (game default)" },
|
|
17
|
+
...TOUCH_STYLE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
|
18
|
+
];
|
|
13
19
|
export function useSettingsCategories(config) {
|
|
14
20
|
const store = useSettingsStore();
|
|
15
21
|
const fov = usePlayerFov();
|
|
@@ -119,11 +125,26 @@ export function useSettingsCategories(config) {
|
|
|
119
125
|
reset: () => config.resetBinding(action),
|
|
120
126
|
};
|
|
121
127
|
});
|
|
128
|
+
const controlsRows = [
|
|
129
|
+
...(config.touchStyle
|
|
130
|
+
? [
|
|
131
|
+
{
|
|
132
|
+
id: SETTING_IDS.touchStyle,
|
|
133
|
+
label: "Touch controls",
|
|
134
|
+
kind: "select",
|
|
135
|
+
value: store.get(SETTING_IDS.touchStyle, TOUCH_STYLE_AUTO),
|
|
136
|
+
options: TOUCH_STYLE_SELECT_OPTIONS,
|
|
137
|
+
set: (value) => store.set(SETTING_IDS.touchStyle, value),
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
: []),
|
|
141
|
+
...extrasFor("controls"),
|
|
142
|
+
];
|
|
122
143
|
const builtInRows = {
|
|
123
144
|
sound: soundRows,
|
|
124
145
|
graphics: graphicsRows,
|
|
125
146
|
gameplay: gameplayRows,
|
|
126
|
-
controls:
|
|
147
|
+
controls: controlsRows,
|
|
127
148
|
};
|
|
128
149
|
const builtInKeybinds = { controls: keybinds };
|
|
129
150
|
const declaredLabels = new Map(config.categories.map((c) => [c.id, c.label]));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
|
|
2
|
+
export declare const EMPTY_RESERVED: ReadonlySet<string>;
|
|
3
|
+
/** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
|
|
4
|
+
export declare const NO_ACTIONS: string[];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
|
|
2
|
+
export const EMPTY_RESERVED = new Set();
|
|
3
|
+
/** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
|
|
4
|
+
export const NO_ACTIONS = [];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Movement action names the shell drives via the walk controller. @internal */
|
|
2
|
+
export declare const SHELL_MOVEMENT_ACTIONS: readonly ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
3
|
+
/**
|
|
4
|
+
* True when the game's input map binds any walk-controller action — the shell owns pose
|
|
5
|
+
* for those games. Games that omit all five drive pose themselves.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function shellDrivesPlayerPose(input: Record<string, unknown> | undefined): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Movement action names the shell drives via the walk controller. @internal */
|
|
2
|
+
export const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
3
|
+
/**
|
|
4
|
+
* True when the game's input map binds any walk-controller action — the shell owns pose
|
|
5
|
+
* for those games. Games that omit all five drive pose themselves.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export function shellDrivesPlayerPose(input) {
|
|
9
|
+
const bound = input ?? {};
|
|
10
|
+
return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ContextMenu } from "@jgengine/core/interaction/contextMenu";
|
|
2
|
+
import type { Aim } from "@jgengine/core/scene/spatial";
|
|
3
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
4
|
+
import type { PointerService } from "./pointer/pointerService.js";
|
|
5
|
+
/** Minimal playable slice for context-menu verb lookup. @internal */
|
|
6
|
+
export type PointerPlayable = {
|
|
7
|
+
content: {
|
|
8
|
+
entityById?: (name: string) => {
|
|
9
|
+
verbs?: ContextMenu["verbs"];
|
|
10
|
+
} | null | undefined;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** Aim from the possessed/local player eye toward the current pointer world hit. @internal */
|
|
14
|
+
export declare function pointerAimFor(ctx: GameContext, service: PointerService): Aim | undefined;
|
|
15
|
+
/** Context menu for an entity/object pointer hit, or null for empty ground. @internal */
|
|
16
|
+
export declare function pointerContextMenu(ctx: GameContext, playable: PointerPlayable, hit: {
|
|
17
|
+
point: readonly [number, number, number];
|
|
18
|
+
entity: string | null;
|
|
19
|
+
object: string | null;
|
|
20
|
+
}): ContextMenu | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { buildContextMenu, } from "@jgengine/core/interaction/contextMenu";
|
|
2
|
+
import { aimToPoint } from "@jgengine/core/input/pointer";
|
|
3
|
+
import { eyeHeightFromColliders } from "@jgengine/core/combat/shotOrigin";
|
|
4
|
+
/** Aim from the possessed/local player eye toward the current pointer world hit. @internal */
|
|
5
|
+
export function pointerAimFor(ctx, service) {
|
|
6
|
+
const hit = service.worldHit();
|
|
7
|
+
if (hit === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
const shooter = ctx.scene.entity.get(ctx.player.possession.active(ctx.player.userId)) ??
|
|
10
|
+
ctx.scene.entity.get(ctx.player.userId);
|
|
11
|
+
if (shooter === null)
|
|
12
|
+
return undefined;
|
|
13
|
+
const eye = eyeHeightFromColliders(ctx.scene.entity.collidersOf(shooter.id));
|
|
14
|
+
return aimToPoint([shooter.position[0], shooter.position[1] + eye, shooter.position[2]], hit.point);
|
|
15
|
+
}
|
|
16
|
+
/** Context menu for an entity/object pointer hit, or null for empty ground. @internal */
|
|
17
|
+
export function pointerContextMenu(ctx, playable, hit) {
|
|
18
|
+
if (hit.entity !== null) {
|
|
19
|
+
const entity = ctx.scene.entity.get(hit.entity);
|
|
20
|
+
const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
|
|
21
|
+
return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
|
|
22
|
+
}
|
|
23
|
+
if (hit.object !== null) {
|
|
24
|
+
const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
|
|
25
|
+
return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -5,4 +5,8 @@ export interface PlacementGhostProps {
|
|
|
5
5
|
validColor?: string;
|
|
6
6
|
invalidColor?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Cursor-following build ghost: valid/invalid tint from a placement controller preview.
|
|
10
|
+
* @capability placement-ghost render valid/invalid placement preview mesh
|
|
11
|
+
*/
|
|
8
12
|
export declare function PlacementGhost({ preview, height, validColor, invalidColor, }: PlacementGhostProps): import("react").JSX.Element | null;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { quarterTurnsToRotationY } from "@jgengine/core/world/placementController";
|
|
4
|
+
/**
|
|
5
|
+
* Cursor-following build ghost: valid/invalid tint from a placement controller preview.
|
|
6
|
+
* @capability placement-ghost render valid/invalid placement preview mesh
|
|
7
|
+
*/
|
|
4
8
|
export function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }) {
|
|
5
9
|
const footprint = preview?.footprint;
|
|
6
10
|
const size = useMemo(() => [footprint?.w ?? 1, height, footprint?.d ?? 1], [footprint?.w, footprint?.d, height]);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Active TransformControls mode for the runtime selection gizmo. */
|
|
2
|
+
export type TransformGizmoMode = "translate" | "rotate" | "scale";
|
|
3
|
+
/** Snap policy: grid quantize, free drag, or ground-height sample on release. */
|
|
4
|
+
export type TransformGizmoSnap = "grid" | "free" | "ground";
|
|
5
|
+
/** Pose reported by {@link TransformGizmo} when the user releases a drag. */
|
|
6
|
+
export interface TransformGizmoPose {
|
|
7
|
+
position: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
z: number;
|
|
11
|
+
};
|
|
12
|
+
rotationY: number;
|
|
13
|
+
scale: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
z: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Props for the shared runtime/editor selection gizmo. */
|
|
20
|
+
export interface TransformGizmoProps {
|
|
21
|
+
position: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
z: number;
|
|
25
|
+
};
|
|
26
|
+
rotationY?: number;
|
|
27
|
+
mode?: TransformGizmoMode;
|
|
28
|
+
snapMode?: TransformGizmoSnap;
|
|
29
|
+
gridSize?: number;
|
|
30
|
+
/** Vertical offset so the gizmo sits above ground markers. */
|
|
31
|
+
lift?: number;
|
|
32
|
+
size?: number;
|
|
33
|
+
groundSnap?: (x: number, z: number) => number;
|
|
34
|
+
/** Disable orbit/pan while dragging (shell/editor cameras). */
|
|
35
|
+
onDraggingChange?: (dragging: boolean) => void;
|
|
36
|
+
onRelease: (pose: TransformGizmoPose) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runtime selection/move gizmo shared by games and the editor.
|
|
40
|
+
* Wraps TransformControls; callers own selection and commit side-effects.
|
|
41
|
+
* @capability transform-gizmo in-game select/move/rotate/scale gizmo
|
|
42
|
+
*/
|
|
43
|
+
export declare const TransformGizmo: import("react").MemoExoticComponent<({ position, rotationY, mode, snapMode, gridSize, lift, size, groundSnap, onDraggingChange, onRelease, }: TransformGizmoProps) => import("react").JSX.Element>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TransformControls } from "@react-three/drei";
|
|
3
|
+
import { useThree } from "@react-three/fiber";
|
|
4
|
+
import { memo, useEffect, useRef, useState } from "react";
|
|
5
|
+
/**
|
|
6
|
+
* Runtime selection/move gizmo shared by games and the editor.
|
|
7
|
+
* Wraps TransformControls; callers own selection and commit side-effects.
|
|
8
|
+
* @capability transform-gizmo in-game select/move/rotate/scale gizmo
|
|
9
|
+
*/
|
|
10
|
+
export const TransformGizmo = memo(function TransformGizmo({ position, rotationY = 0, mode = "translate", snapMode = "free", gridSize = 1, lift = 0, size = 0.85, groundSnap, onDraggingChange, onRelease, }) {
|
|
11
|
+
const groupRef = useRef(null);
|
|
12
|
+
const [object, setObject] = useState(null);
|
|
13
|
+
const draggingRef = useRef(false);
|
|
14
|
+
const controls = useThree((state) => state.controls);
|
|
15
|
+
const anchorKey = `${position.x}:${position.y}:${position.z}:${rotationY}:${mode}`;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setObject(groupRef.current);
|
|
18
|
+
}, [anchorKey]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const group = groupRef.current;
|
|
21
|
+
if (group === null || draggingRef.current)
|
|
22
|
+
return;
|
|
23
|
+
group.position.set(position.x, position.y + lift, position.z);
|
|
24
|
+
group.rotation.set(0, rotationY, 0);
|
|
25
|
+
group.scale.set(1, 1, 1);
|
|
26
|
+
}, [position.x, position.y, position.z, rotationY, lift, anchorKey, mode]);
|
|
27
|
+
const snapProps = snapMode === "grid"
|
|
28
|
+
? { translationSnap: gridSize, rotationSnap: Math.PI / 12 }
|
|
29
|
+
: { rotationSnap: Math.PI / 12 };
|
|
30
|
+
const handleRelease = () => {
|
|
31
|
+
draggingRef.current = false;
|
|
32
|
+
if (controls !== null && "enabled" in controls)
|
|
33
|
+
controls.enabled = true;
|
|
34
|
+
onDraggingChange?.(false);
|
|
35
|
+
const current = groupRef.current;
|
|
36
|
+
if (current === null)
|
|
37
|
+
return;
|
|
38
|
+
let y = current.position.y - lift;
|
|
39
|
+
if (snapMode === "ground" && groundSnap !== undefined) {
|
|
40
|
+
y = groundSnap(current.position.x, current.position.z);
|
|
41
|
+
current.position.y = y + lift;
|
|
42
|
+
}
|
|
43
|
+
onRelease({
|
|
44
|
+
position: { x: current.position.x, y, z: current.position.z },
|
|
45
|
+
rotationY: current.rotation.y,
|
|
46
|
+
scale: { x: current.scale.x, y: current.scale.y, z: current.scale.z },
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, ...snapProps, onMouseDown: () => {
|
|
50
|
+
draggingRef.current = true;
|
|
51
|
+
if (controls !== null && "enabled" in controls)
|
|
52
|
+
controls.enabled = false;
|
|
53
|
+
onDraggingChange?.(true);
|
|
54
|
+
}, onMouseUp: handleRelease })) : null] }));
|
|
55
|
+
});
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { BuildingBlock, GeneratedBuilding, InstancedBuildings, type BuildingBlockProps, type GeneratedBuildingProps, type InstancedBuildingPlacement, type InstancedBuildingsProps, } from "./GeneratedBuilding.js";
|
|
2
2
|
export { PlacementGhost, type PlacementGhostProps } from "./PlacementGhost.js";
|
|
3
|
+
export { TransformGizmo, type TransformGizmoMode, type TransformGizmoPose, type TransformGizmoProps, type TransformGizmoSnap, } from "./TransformGizmo.js";
|
package/dist/structures/index.js
CHANGED
|
@@ -23,7 +23,7 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
|
|
|
23
23
|
wind,
|
|
24
24
|
roughness,
|
|
25
25
|
}), [colorBase, colorTip, colorVariation, roughness, wind]);
|
|
26
|
-
geometry.instanceCount = resolveGrassInstanceBudget(count, density, budget);
|
|
26
|
+
geometry.instanceCount = resolveGrassInstanceBudget(count, density, area, budget);
|
|
27
27
|
useFrame((state) => {
|
|
28
28
|
handle.uniforms.uTime.value = state.clock.elapsedTime;
|
|
29
29
|
});
|
|
@@ -2,7 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
const MODE_COLOR = {
|
|
3
3
|
raise: "#38bdf8",
|
|
4
4
|
lower: "#fb923c",
|
|
5
|
+
smooth: "#c084fc",
|
|
5
6
|
flatten: "#a3e635",
|
|
7
|
+
noise: "#facc15",
|
|
8
|
+
ramp: "#2dd4bf",
|
|
6
9
|
paint: "#e879f9",
|
|
7
10
|
};
|
|
8
11
|
export function TerraformBrushCursor({ center, y = 0.05, radius, mode }) {
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { type TerrainArea } from "./terrainMath.js";
|
|
1
2
|
export declare const DEFAULT_GRASS_COUNT = 1500;
|
|
2
|
-
|
|
3
|
-
export declare
|
|
3
|
+
/** Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means. */
|
|
4
|
+
export declare const DEFAULT_GRASS_DENSITY = 4;
|
|
5
|
+
/**
|
|
6
|
+
* Blade instance count for a patch: `density` (blades/m²) times the patch's
|
|
7
|
+
* `area`, capped at `budget` (defaulting to `count`, the buffer's allocated
|
|
8
|
+
* capacity) so a big field never exceeds the perf ceiling — it just renders
|
|
9
|
+
* sparser than requested and logs a devtools warning when that happens.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveGrassInstanceBudget(count: number, density: number, area: TerrainArea, budget?: number): number;
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
2
|
+
import { resolveTerrainSize } from "./terrainMath.js";
|
|
1
3
|
export const DEFAULT_GRASS_COUNT = 1500;
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means. */
|
|
5
|
+
export const DEFAULT_GRASS_DENSITY = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Blade instance count for a patch: `density` (blades/m²) times the patch's
|
|
8
|
+
* `area`, capped at `budget` (defaulting to `count`, the buffer's allocated
|
|
9
|
+
* capacity) so a big field never exceeds the perf ceiling — it just renders
|
|
10
|
+
* sparser than requested and logs a devtools warning when that happens.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function resolveGrassInstanceBudget(count, density, area, budget) {
|
|
14
|
+
const size = resolveTerrainSize(area);
|
|
15
|
+
const areaSqMeters = Math.max(0, size.width) * Math.max(0, size.depth);
|
|
16
|
+
const requested = Math.max(0, density) * areaSqMeters;
|
|
17
|
+
const cap = budget === undefined ? count : Math.min(count, Math.max(0, Math.floor(budget)));
|
|
18
|
+
if (requested > cap) {
|
|
19
|
+
devtools.logs.push("warn", `[jgengine:grass] density ${density} blades/m² over ${areaSqMeters.toFixed(0)}m² wants ${Math.floor(requested)} blades — clamped to budget ${cap}`);
|
|
20
|
+
}
|
|
21
|
+
return Math.floor(Math.min(Math.max(0, cap), requested));
|
|
6
22
|
}
|