@jgengine/shell 0.7.0 → 0.9.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 +63 -0
- package/README.md +37 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +36 -1
- package/dist/GamePlayerShell.js +751 -80
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +12 -0
- package/dist/behaviour.js +27 -0
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +16 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.js +10 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +8 -1
- package/dist/camera/cameraRigs.js +78 -38
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +7 -2
- package/dist/camera/index.js +7 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +50 -1
- package/dist/camera/rigMath.js +91 -1
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +245 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +52 -0
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
- package/dist/devtools/DevtoolsOverlay.js +676 -0
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +56 -0
- package/dist/environment/Daylight.js +101 -0
- package/dist/environment/EnvironmentScene.js +87 -48
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +3 -0
- package/dist/environment/index.js +3 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/materialOverride.d.ts +6 -0
- package/dist/materialOverride.js +28 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +20 -3
- package/dist/pointer/pointerService.d.ts +5 -0
- package/dist/pointer/pointerService.js +51 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +23 -0
- package/dist/render/modelRender.js +106 -0
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +9 -0
- package/dist/terrain/terrainMath.js +17 -2
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +34 -0
- package/dist/touch/TouchControlsOverlay.js +279 -0
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +77 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +8 -1
- package/dist/world/WorldHud.js +90 -39
- package/dist/world/WorldItems.js +21 -2
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +51 -0
- package/llms.txt +2643 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -5,22 +5,39 @@ export function PointerProbe({ service }) {
|
|
|
5
5
|
const scene = useThree((state) => state.scene);
|
|
6
6
|
const gl = useThree((state) => state.gl);
|
|
7
7
|
const size = useThree((state) => state.size);
|
|
8
|
-
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
service.bind({ camera, scene, width: size.width, height: size.height });
|
|
10
|
+
return () => {
|
|
11
|
+
service.bind(null);
|
|
12
|
+
};
|
|
13
|
+
}, [service, camera, scene, size.width, size.height]);
|
|
9
14
|
useEffect(() => {
|
|
10
15
|
const el = gl.domElement;
|
|
16
|
+
let locked = false;
|
|
11
17
|
const onMove = (event) => {
|
|
18
|
+
if (locked)
|
|
19
|
+
return;
|
|
12
20
|
const rect = el.getBoundingClientRect();
|
|
13
21
|
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
14
22
|
const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1);
|
|
15
23
|
service.setCursor(x, y, true);
|
|
16
24
|
};
|
|
17
|
-
const onLeave = () =>
|
|
25
|
+
const onLeave = () => {
|
|
26
|
+
if (!locked)
|
|
27
|
+
service.setCursor(0, 0, false);
|
|
28
|
+
};
|
|
29
|
+
const onLockChange = () => {
|
|
30
|
+
locked = document.pointerLockElement === el;
|
|
31
|
+
if (locked)
|
|
32
|
+
service.setCursor(0, 0, true);
|
|
33
|
+
};
|
|
18
34
|
el.addEventListener("pointermove", onMove);
|
|
19
35
|
el.addEventListener("pointerleave", onLeave);
|
|
36
|
+
document.addEventListener("pointerlockchange", onLockChange);
|
|
20
37
|
return () => {
|
|
21
38
|
el.removeEventListener("pointermove", onMove);
|
|
22
39
|
el.removeEventListener("pointerleave", onLeave);
|
|
23
|
-
|
|
40
|
+
document.removeEventListener("pointerlockchange", onLockChange);
|
|
24
41
|
};
|
|
25
42
|
}, [gl, service]);
|
|
26
43
|
return null;
|
|
@@ -2,15 +2,20 @@ import * as THREE from "three";
|
|
|
2
2
|
import type { PointerHit, PointerVec3 } from "@jgengine/core/input/pointer";
|
|
3
3
|
export declare const POINTER_ENTITY_KEY = "jgEntityId";
|
|
4
4
|
export declare const POINTER_OBJECT_KEY = "jgObjectId";
|
|
5
|
+
export type PointerHitFilter = (object: THREE.Object3D) => boolean;
|
|
5
6
|
interface PointerDeps {
|
|
6
7
|
camera: THREE.Camera;
|
|
7
8
|
scene: THREE.Scene;
|
|
8
9
|
width: number;
|
|
9
10
|
height: number;
|
|
11
|
+
layers?: THREE.Layers;
|
|
12
|
+
filter?: PointerHitFilter;
|
|
10
13
|
}
|
|
11
14
|
export interface PointerService {
|
|
12
15
|
/** Cast the current cursor into the world; null when the cursor is off-canvas. */
|
|
13
16
|
worldHit(): PointerHit | null;
|
|
17
|
+
/** Cast from the viewport center regardless of cursor presence (pointer-lock aim); null before the probe binds. */
|
|
18
|
+
worldHitCenter(): PointerHit | null;
|
|
14
19
|
/** Project a world point to CSS pixels for the marquee / HUD; null before the probe binds. */
|
|
15
20
|
screenOf(world: PointerVec3): {
|
|
16
21
|
x: number;
|
|
@@ -11,14 +11,60 @@ function tagOf(object, key) {
|
|
|
11
11
|
}
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
+
function standardMaterialSample(material) {
|
|
15
|
+
const candidates = Array.isArray(material) ? material : [material];
|
|
16
|
+
const standard = candidates.find((entry) => entry instanceof THREE.MeshStandardMaterial);
|
|
17
|
+
if (standard === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return { color: `#${standard.color.getHexString()}`, metalness: standard.metalness, roughness: standard.roughness };
|
|
20
|
+
}
|
|
14
21
|
export function createPointerService() {
|
|
15
22
|
const raycaster = new THREE.Raycaster();
|
|
16
23
|
const ndc = new THREE.Vector2();
|
|
24
|
+
const centerNdc = new THREE.Vector2(0, 0);
|
|
17
25
|
const scratch = new THREE.Vector3();
|
|
18
26
|
const normalMatrix = new THREE.Matrix3();
|
|
19
27
|
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
20
28
|
let deps = null;
|
|
21
29
|
let cursorPresent = false;
|
|
30
|
+
function hitAtNdc(target) {
|
|
31
|
+
if (deps === null)
|
|
32
|
+
return null;
|
|
33
|
+
if (deps.layers !== undefined)
|
|
34
|
+
raycaster.layers.mask = deps.layers.mask;
|
|
35
|
+
else
|
|
36
|
+
raycaster.layers.enableAll();
|
|
37
|
+
raycaster.setFromCamera(target, deps.camera);
|
|
38
|
+
const intersects = raycaster.intersectObjects(deps.scene.children, true);
|
|
39
|
+
for (const hit of intersects) {
|
|
40
|
+
if (deps.filter !== undefined && !deps.filter(hit.object))
|
|
41
|
+
continue;
|
|
42
|
+
const mesh = hit.object;
|
|
43
|
+
if (!mesh.isMesh)
|
|
44
|
+
continue;
|
|
45
|
+
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
46
|
+
let normal = [0, 1, 0];
|
|
47
|
+
if (hit.face !== null && hit.face !== undefined) {
|
|
48
|
+
normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
49
|
+
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
50
|
+
normal = [scratch.x, scratch.y, scratch.z];
|
|
51
|
+
}
|
|
52
|
+
const uv = hit.uv !== undefined ? { u: hit.uv.x, v: hit.uv.y } : undefined;
|
|
53
|
+
return {
|
|
54
|
+
point,
|
|
55
|
+
normal,
|
|
56
|
+
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
57
|
+
object: tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
58
|
+
...(uv !== undefined ? { uv } : {}),
|
|
59
|
+
material: standardMaterialSample(mesh.material),
|
|
60
|
+
...(hit.instanceId !== undefined ? { instanceId: hit.instanceId } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const grounded = raycaster.ray.intersectPlane(groundPlane, scratch);
|
|
64
|
+
if (grounded === null)
|
|
65
|
+
return null;
|
|
66
|
+
return { point: [grounded.x, grounded.y, grounded.z], normal: [0, 1, 0], entity: null, object: null };
|
|
67
|
+
}
|
|
22
68
|
return {
|
|
23
69
|
hasCursor: () => cursorPresent,
|
|
24
70
|
bind: (next) => {
|
|
@@ -29,31 +75,12 @@ export function createPointerService() {
|
|
|
29
75
|
cursorPresent = present;
|
|
30
76
|
},
|
|
31
77
|
worldHit() {
|
|
32
|
-
if (
|
|
33
|
-
return null;
|
|
34
|
-
raycaster.setFromCamera(ndc, deps.camera);
|
|
35
|
-
const intersects = raycaster.intersectObjects(deps.scene.children, true);
|
|
36
|
-
for (const hit of intersects) {
|
|
37
|
-
if (!hit.object.isMesh)
|
|
38
|
-
continue;
|
|
39
|
-
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
40
|
-
let normal = [0, 1, 0];
|
|
41
|
-
if (hit.face !== null && hit.face !== undefined) {
|
|
42
|
-
normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
43
|
-
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
44
|
-
normal = [scratch.x, scratch.y, scratch.z];
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
point,
|
|
48
|
-
normal,
|
|
49
|
-
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
50
|
-
object: tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const grounded = raycaster.ray.intersectPlane(groundPlane, scratch);
|
|
54
|
-
if (grounded === null)
|
|
78
|
+
if (!cursorPresent)
|
|
55
79
|
return null;
|
|
56
|
-
return
|
|
80
|
+
return hitAtNdc(ndc);
|
|
81
|
+
},
|
|
82
|
+
worldHitCenter() {
|
|
83
|
+
return hitAtNdc(centerNdc);
|
|
57
84
|
},
|
|
58
85
|
screenOf(world) {
|
|
59
86
|
if (deps === null)
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import type { PlayableGame as EnginePlayableGame } from "@jgengine/core/game/playableGame";
|
|
3
3
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
4
5
|
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
5
|
-
export type
|
|
6
|
+
export type RenderObject = (object: SceneObject) => ReactNode;
|
|
7
|
+
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject>;
|
|
6
8
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
9
|
+
export declare function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined;
|
package/dist/registry.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
|
|
3
|
+
export declare const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
+
export declare function cloneModelScene(source: THREE.Object3D, options?: {
|
|
5
|
+
cloneMaterials?: boolean;
|
|
6
|
+
}): THREE.Object3D;
|
|
7
|
+
export declare function disposeClonedMaterials(root: THREE.Object3D): void;
|
|
8
|
+
export declare function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[];
|
|
9
|
+
export interface MaterialCache {
|
|
10
|
+
materials: THREE.MeshStandardMaterial[];
|
|
11
|
+
seedColor: THREE.Color;
|
|
12
|
+
}
|
|
13
|
+
export declare function cacheStandardMaterials(root: THREE.Object3D, into?: MaterialCache | null): MaterialCache;
|
|
14
|
+
export declare function applyPaintTextureToMaterials(materials: readonly THREE.MeshStandardMaterial[], paint: PaintCanvas): void;
|
|
15
|
+
export interface PaintCanvas {
|
|
16
|
+
canvas: HTMLCanvasElement;
|
|
17
|
+
context: CanvasRenderingContext2D;
|
|
18
|
+
texture: THREE.CanvasTexture;
|
|
19
|
+
}
|
|
20
|
+
export declare function createPaintCanvas(seed: THREE.MeshStandardMaterial, size?: number): PaintCanvas;
|
|
21
|
+
export declare function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void;
|
|
22
|
+
export declare function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void;
|
|
23
|
+
export declare function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
3
|
+
export const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
+
export function cloneModelScene(source, options) {
|
|
5
|
+
const clone = cloneSkinned(source);
|
|
6
|
+
if (options?.cloneMaterials === false)
|
|
7
|
+
return clone;
|
|
8
|
+
clone.traverse((node) => {
|
|
9
|
+
const mesh = node;
|
|
10
|
+
if (!mesh.isMesh)
|
|
11
|
+
return;
|
|
12
|
+
mesh.material = Array.isArray(mesh.material)
|
|
13
|
+
? mesh.material.map((material) => material.clone())
|
|
14
|
+
: mesh.material.clone();
|
|
15
|
+
});
|
|
16
|
+
return clone;
|
|
17
|
+
}
|
|
18
|
+
export function disposeClonedMaterials(root) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
root.traverse((node) => {
|
|
21
|
+
const mesh = node;
|
|
22
|
+
if (!mesh.isMesh)
|
|
23
|
+
return;
|
|
24
|
+
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
25
|
+
for (const material of list) {
|
|
26
|
+
if (seen.has(material))
|
|
27
|
+
continue;
|
|
28
|
+
seen.add(material);
|
|
29
|
+
material.dispose();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function isMeshStandardMaterial(material) {
|
|
34
|
+
return material.isMeshStandardMaterial === true;
|
|
35
|
+
}
|
|
36
|
+
export function standardMaterialsOf(root) {
|
|
37
|
+
const materials = [];
|
|
38
|
+
root.traverse((node) => {
|
|
39
|
+
const mesh = node;
|
|
40
|
+
if (!mesh.isMesh)
|
|
41
|
+
return;
|
|
42
|
+
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
43
|
+
for (const material of list)
|
|
44
|
+
if (isMeshStandardMaterial(material))
|
|
45
|
+
materials.push(material);
|
|
46
|
+
});
|
|
47
|
+
return materials;
|
|
48
|
+
}
|
|
49
|
+
export function cacheStandardMaterials(root, into) {
|
|
50
|
+
if (into !== null && into !== undefined)
|
|
51
|
+
return into;
|
|
52
|
+
const materials = standardMaterialsOf(root);
|
|
53
|
+
const seedColor = materials[0]?.color.clone() ?? new THREE.Color("#ffffff");
|
|
54
|
+
return { materials, seedColor };
|
|
55
|
+
}
|
|
56
|
+
export function applyPaintTextureToMaterials(materials, paint) {
|
|
57
|
+
for (const material of materials) {
|
|
58
|
+
material.map = paint.texture;
|
|
59
|
+
material.needsUpdate = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
|
|
63
|
+
const canvas = document.createElement("canvas");
|
|
64
|
+
canvas.width = size;
|
|
65
|
+
canvas.height = size;
|
|
66
|
+
const context = canvas.getContext("2d");
|
|
67
|
+
const seedImage = seed.map?.image;
|
|
68
|
+
if (seedImage !== undefined && seedImage.width) {
|
|
69
|
+
context.drawImage(seedImage, 0, 0, size, size);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
context.fillStyle = `#${seed.color.getHexString()}`;
|
|
73
|
+
context.fillRect(0, 0, size, size);
|
|
74
|
+
}
|
|
75
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
76
|
+
texture.colorSpace = seed.map?.colorSpace ?? THREE.SRGBColorSpace;
|
|
77
|
+
return { canvas, context, texture };
|
|
78
|
+
}
|
|
79
|
+
export function drawPaintStrokes(paint, strokes) {
|
|
80
|
+
const { canvas, context, texture } = paint;
|
|
81
|
+
for (const stroke of strokes) {
|
|
82
|
+
context.fillStyle = stroke.color;
|
|
83
|
+
context.beginPath();
|
|
84
|
+
context.arc(stroke.u * canvas.width, (1 - stroke.v) * canvas.height, stroke.radius * canvas.width, 0, Math.PI * 2);
|
|
85
|
+
context.fill();
|
|
86
|
+
}
|
|
87
|
+
texture.needsUpdate = true;
|
|
88
|
+
}
|
|
89
|
+
export function applyPaintTexture(root, paint) {
|
|
90
|
+
applyPaintTextureToMaterials(standardMaterialsOf(root), paint);
|
|
91
|
+
}
|
|
92
|
+
export function syncPaintCanvas(paint, seedColor, strokes, drawnCount) {
|
|
93
|
+
if (strokes.length < drawnCount) {
|
|
94
|
+
const { canvas, context } = paint;
|
|
95
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
96
|
+
context.fillStyle = `#${seedColor.getHexString()}`;
|
|
97
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
98
|
+
drawPaintStrokes(paint, strokes);
|
|
99
|
+
return strokes.length;
|
|
100
|
+
}
|
|
101
|
+
if (strokes.length > drawnCount) {
|
|
102
|
+
drawPaintStrokes(paint, strokes.slice(drawnCount));
|
|
103
|
+
return strokes.length;
|
|
104
|
+
}
|
|
105
|
+
return drawnCount;
|
|
106
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
2
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
export interface ModelResolveContext {
|
|
4
|
+
seam: "entityModels" | "objectModels";
|
|
5
|
+
key: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
|
|
9
|
+
* ids throw — silent generic-primitive fallback only happens when the mapping
|
|
10
|
+
* omits the key entirely (or uses tryResolveCatalogModel for optional ids).
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveModel(value: string | ModelConfig | undefined, assets: AssetCatalog, context?: ModelResolveContext): ModelConfig | undefined;
|
|
13
|
+
/** Soft lookup used when an object catalog id may double as a model asset id. */
|
|
14
|
+
export declare function tryResolveCatalogModel(id: string, assets: AssetCatalog): ModelConfig | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
|
|
3
|
+
* ids throw — silent generic-primitive fallback only happens when the mapping
|
|
4
|
+
* omits the key entirely (or uses tryResolveCatalogModel for optional ids).
|
|
5
|
+
*/
|
|
6
|
+
export function resolveModel(value, assets, context) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof value !== "string")
|
|
10
|
+
return value;
|
|
11
|
+
const ref = assets.resolve(value);
|
|
12
|
+
if (ref === null) {
|
|
13
|
+
const where = context === undefined ? `model asset "${value}"` : `${context.seam}["${context.key}"] → "${value}"`;
|
|
14
|
+
throw new Error(`[jgengine] missing ${where} — not in the asset catalog. Fix the id, register it, or omit the mapping to keep the deliberate primitive/sprite fallback.`);
|
|
15
|
+
}
|
|
16
|
+
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
17
|
+
}
|
|
18
|
+
/** Soft lookup used when an object catalog id may double as a model asset id. */
|
|
19
|
+
export function tryResolveCatalogModel(id, assets) {
|
|
20
|
+
const ref = assets.resolve(id);
|
|
21
|
+
if (ref === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
function QuickSlider({ row }) {
|
|
4
|
+
const value = Number(row.value);
|
|
5
|
+
const readout = row.format?.(value) ?? String(value);
|
|
6
|
+
return (_jsxs("label", { className: "flex items-center gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "w-16 shrink-0 truncate", children: row.label }), _jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: value, "aria-label": row.label, className: "h-1.5 flex-1 cursor-pointer accent-emerald-400", onChange: (event) => row.set(Number(event.target.value)) }), _jsx("span", { className: "w-9 shrink-0 text-right tabular-nums text-neutral-100", children: readout })] }));
|
|
7
|
+
}
|
|
8
|
+
function QuickToggle({ row }) {
|
|
9
|
+
const on = Boolean(row.value);
|
|
10
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("button", { type: "button", role: "switch", "aria-checked": on, "aria-label": row.label, onClick: () => row.set(!on), className: `relative h-6 w-11 shrink-0 rounded-full ring-1 transition ${on ? "bg-emerald-500 ring-emerald-400" : "bg-neutral-700 ring-white/10"}`, children: _jsx("span", { className: `absolute top-1 h-4 w-4 rounded-full bg-white shadow transition-all ${on ? "left-6" : "left-1"}` }) })] }));
|
|
11
|
+
}
|
|
12
|
+
function QuickSelect({ row }) {
|
|
13
|
+
const options = row.options ?? [];
|
|
14
|
+
const current = String(row.value);
|
|
15
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "truncate", children: row.label }), _jsx("div", { className: "flex shrink-0 gap-1 rounded-md bg-neutral-950/60 p-0.5 ring-1 ring-white/10", children: options.map((option) => {
|
|
16
|
+
const selected = option.value === current;
|
|
17
|
+
return (_jsx("button", { type: "button", onClick: () => row.set(option.value), className: `rounded px-2 py-1 font-medium transition ${selected ? "bg-emerald-500 text-neutral-950" : "text-neutral-300 hover:bg-neutral-800"}`, children: option.label }, option.value));
|
|
18
|
+
}) })] }));
|
|
19
|
+
}
|
|
20
|
+
function QuickRow({ row }) {
|
|
21
|
+
if (row.kind === "slider")
|
|
22
|
+
return _jsx(QuickSlider, { row: row });
|
|
23
|
+
if (row.kind === "toggle")
|
|
24
|
+
return _jsx(QuickToggle, { row: row });
|
|
25
|
+
return _jsx(QuickSelect, { row: row });
|
|
26
|
+
}
|
|
27
|
+
function Popover({ children }) {
|
|
28
|
+
return (_jsx("div", { className: "absolute bottom-12 left-0 w-64 space-y-2.5 rounded-lg bg-neutral-950/90 p-3 shadow-2xl ring-1 ring-white/10 backdrop-blur-sm", children: children }));
|
|
29
|
+
}
|
|
30
|
+
function QuickButton({ label, open, onToggle, children, }) {
|
|
31
|
+
return (_jsx("button", { type: "button", onClick: onToggle, "aria-label": label, "aria-pressed": open, className: `flex h-11 w-11 items-center justify-center rounded-md text-lg shadow-lg ring-1 backdrop-blur-sm transition ${open ? "bg-emerald-500/20 text-emerald-300 ring-emerald-400/50" : "bg-neutral-950/75 text-neutral-200 ring-white/10 hover:bg-neutral-900/80"}`, children: children }));
|
|
32
|
+
}
|
|
33
|
+
export function QuickControls({ controller }) {
|
|
34
|
+
const [open, setOpen] = useState(null);
|
|
35
|
+
const byId = (id) => controller.categories.find((category) => category.id === id);
|
|
36
|
+
const sound = byId("sound");
|
|
37
|
+
const graphics = byId("graphics");
|
|
38
|
+
if (sound === undefined && graphics === undefined)
|
|
39
|
+
return null;
|
|
40
|
+
const toggle = (which) => setOpen((current) => (current === which ? null : which));
|
|
41
|
+
return (_jsxs("div", { className: "pointer-events-auto absolute bottom-3 left-3 z-30 flex items-end gap-2", children: [sound !== undefined ? (_jsxs("div", { className: "relative", children: [open === "volume" ? (_jsx(Popover, { children: sound.rows.map((row) => (_jsx(QuickRow, { row: row }, row.id))) })) : null, _jsx(QuickButton, { label: "Volume", open: open === "volume", onToggle: () => toggle("volume"), children: _jsx("span", { "aria-hidden": true, children: "\uD83D\uDD0A" }) })] })) : null, graphics !== undefined ? (_jsxs("div", { className: "relative", children: [open === "graphics" ? (_jsx(Popover, { children: graphics.rows.map((row) => (_jsx(QuickRow, { row: row }, row.id))) })) : null, _jsx(QuickButton, { label: "Graphics", open: open === "graphics", onToggle: () => toggle("graphics"), children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", className: "h-5 w-5", "aria-hidden": true, children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 3a9 9 0 0 0 0 18M3 12h18M5 6c2 1.5 5 2.5 7 2.5S17 7.5 19 6M5 18c2-1.5 5-2.5 7-2.5s5 1 7 2.5" })] }) })] })) : null] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function SettingsChrome(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSettings } from "@jgengine/react/settings";
|
|
3
|
+
import { QuickControls } from "./QuickControls.js";
|
|
4
|
+
import { SettingsMenu } from "./SettingsMenu.js";
|
|
5
|
+
export function SettingsChrome() {
|
|
6
|
+
const settings = useSettings();
|
|
7
|
+
if (settings.categories.length === 0 && settings.actions.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
return (_jsxs(_Fragment, { children: [settings.surface === "quick" ? _jsx(QuickControls, { controller: settings }) : null, settings.isOpen ? _jsx(SettingsMenu, { controller: settings, onClose: settings.close }) : null] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import {} from "./settingsController.js";
|
|
4
|
+
const surface = "var(--jg-surface, #16161d)";
|
|
5
|
+
const surfaceDeep = "var(--jg-surface-deep, #0b0b11)";
|
|
6
|
+
const edge = "var(--jg-edge, #2b2b35)";
|
|
7
|
+
const text = "var(--jg-text, #ececef)";
|
|
8
|
+
const textDim = "var(--jg-text-dim, #9494a0)";
|
|
9
|
+
const accent = "var(--jg-accent, #6ee7a8)";
|
|
10
|
+
const accentGlow = "var(--jg-accent-glow, rgba(110,231,168,0.35))";
|
|
11
|
+
const onAccent = "var(--jg-surface-deep, #0b0b0f)";
|
|
12
|
+
const danger = "var(--jg-danger, #e0574e)";
|
|
13
|
+
const fontDisplay = "var(--jg-font-display, inherit)";
|
|
14
|
+
const rowStyle = { background: "rgba(255,255,255,0.045)", border: "1px solid rgba(255,255,255,0.08)" };
|
|
15
|
+
const badgeStyle = { background: surfaceDeep, border: "1px solid rgba(255,255,255,0.12)", color: text };
|
|
16
|
+
function GlyphClose() {
|
|
17
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", className: "h-4 w-4", "aria-hidden": true, children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }));
|
|
18
|
+
}
|
|
19
|
+
function GlyphReset() {
|
|
20
|
+
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-3.5 w-3.5", "aria-hidden": true, children: [_jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7L3 8" }), _jsx("path", { d: "M3 3v5h5" })] }));
|
|
21
|
+
}
|
|
22
|
+
function CloseButton({ onClose }) {
|
|
23
|
+
return (_jsx("button", { type: "button", onClick: onClose, "aria-label": "Close settings", className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition hover:bg-white/10", style: { color: textDim, border: "1px solid rgba(255,255,255,0.1)" }, children: _jsx(GlyphClose, {}) }));
|
|
24
|
+
}
|
|
25
|
+
function RowShell({ label, control }) {
|
|
26
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-4 rounded-xl px-3.5 py-3", style: rowStyle, children: [_jsx("span", { className: "text-sm", style: { color: text }, children: label }), control] }));
|
|
27
|
+
}
|
|
28
|
+
function SliderRow({ row }) {
|
|
29
|
+
const value = Number(row.value);
|
|
30
|
+
const readout = row.format?.(value) ?? String(value);
|
|
31
|
+
return (_jsx(RowShell, { label: row.label, control: _jsxs("div", { className: "flex flex-1 items-center gap-3 pl-4", children: [_jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: value, "aria-label": row.label, className: "h-1.5 flex-1 cursor-pointer", style: { accentColor: accent }, onChange: (event) => row.set(Number(event.target.value)) }), _jsx("span", { className: "w-12 shrink-0 text-right text-sm tabular-nums", style: { color: text }, children: readout })] }) }));
|
|
32
|
+
}
|
|
33
|
+
function ToggleRow({ row }) {
|
|
34
|
+
const on = Boolean(row.value);
|
|
35
|
+
return (_jsx(RowShell, { label: row.label, control: _jsx("button", { type: "button", role: "switch", "aria-checked": on, "aria-label": row.label, onClick: () => row.set(!on), className: "relative h-7 w-12 shrink-0 rounded-full transition", style: { background: on ? accent : "rgba(255,255,255,0.16)", boxShadow: on ? `0 0 12px ${accentGlow}` : "none" }, children: _jsx("span", { className: `absolute top-1 h-5 w-5 rounded-full bg-white shadow transition-all ${on ? "left-6" : "left-1"}` }) }) }));
|
|
36
|
+
}
|
|
37
|
+
function SelectRow({ row }) {
|
|
38
|
+
const options = row.options ?? [];
|
|
39
|
+
const current = String(row.value);
|
|
40
|
+
return (_jsx(RowShell, { label: row.label, control: _jsx("div", { className: "flex shrink-0 gap-1 rounded-lg p-1", style: { background: surfaceDeep }, children: options.map((option) => {
|
|
41
|
+
const selected = option.value === current;
|
|
42
|
+
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));
|
|
43
|
+
}) }) }));
|
|
44
|
+
}
|
|
45
|
+
function RowControl({ row }) {
|
|
46
|
+
if (row.kind === "slider")
|
|
47
|
+
return _jsx(SliderRow, { row: row });
|
|
48
|
+
if (row.kind === "toggle")
|
|
49
|
+
return _jsx(ToggleRow, { row: row });
|
|
50
|
+
return _jsx(SelectRow, { row: row });
|
|
51
|
+
}
|
|
52
|
+
function KeybindRowView({ row, capturing, onCapture, }) {
|
|
53
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-3 rounded-xl px-3.5 py-3", style: rowStyle, children: [_jsx("span", { className: "text-sm", style: { color: text }, children: row.label }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx("button", { type: "button", onClick: onCapture, disabled: capturing, title: "Click to rebind \u2014 Esc to cancel", className: "inline-flex min-h-[34px] min-w-[3.75rem] items-center justify-center rounded-lg px-3 font-mono text-sm transition hover:brightness-125", style: capturing
|
|
54
|
+
? { background: accentGlow, color: accent, border: `1px solid ${accent}` }
|
|
55
|
+
: badgeStyle, children: capturing ? "Press…" : row.bindingLabel }), _jsx("button", { type: "button", onClick: () => row.reset(), "aria-label": `Reset ${row.label}`, title: "Reset to default", className: `flex h-8 w-8 items-center justify-center rounded-lg transition hover:bg-white/10 ${row.isDefault ? "invisible" : ""}`, style: { color: textDim }, children: _jsx(GlyphReset, {}) })] })] }));
|
|
56
|
+
}
|
|
57
|
+
function ActionRow({ action, onClose }) {
|
|
58
|
+
const isDanger = action.kind === "danger";
|
|
59
|
+
return (_jsxs("button", { type: "button", onClick: () => {
|
|
60
|
+
action.run();
|
|
61
|
+
onClose();
|
|
62
|
+
}, className: "flex w-full items-center justify-between gap-3 rounded-xl px-4 py-3.5 text-left transition hover:brightness-110", style: {
|
|
63
|
+
background: isDanger ? "rgba(224,87,78,0.12)" : "rgba(255,255,255,0.05)",
|
|
64
|
+
border: `1px solid ${isDanger ? danger : "rgba(255,255,255,0.1)"}`,
|
|
65
|
+
}, 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" }) })] }));
|
|
66
|
+
}
|
|
67
|
+
function TabButton({ tab, selected, orientation, onSelect }) {
|
|
68
|
+
return (_jsx("button", { type: "button", onClick: onSelect, className: `min-h-[40px] whitespace-nowrap rounded-lg px-4 text-sm font-medium transition ${orientation === "col" ? "w-full text-left" : ""} ${selected ? "" : "hover:bg-white/5"}`, style: selected ? { background: accent, color: onAccent } : { color: textDim }, children: tab.label }));
|
|
69
|
+
}
|
|
70
|
+
function Body({ tab, actions, capturing, setCapturing, onClose }) {
|
|
71
|
+
return (_jsx("div", { className: "jg-settings-scroll flex-1 space-y-2 overflow-y-auto p-4", children: tab.category === null
|
|
72
|
+
? actions.map((action) => _jsx(ActionRow, { action: action, onClose: onClose }, action.id))
|
|
73
|
+
: (_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)))] })) }));
|
|
74
|
+
}
|
|
75
|
+
const panelStyle = {
|
|
76
|
+
background: surface,
|
|
77
|
+
color: text,
|
|
78
|
+
border: `1px solid ${edge}`,
|
|
79
|
+
fontFamily: fontDisplay,
|
|
80
|
+
};
|
|
81
|
+
function Backdrop({ children, onClose, align }) {
|
|
82
|
+
return (_jsx("div", { className: `pointer-events-auto absolute inset-0 z-40 flex justify-center bg-black/55 p-3 backdrop-blur-sm ${align}`, onClick: onClose, children: children }));
|
|
83
|
+
}
|
|
84
|
+
function stop(event) {
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
}
|
|
87
|
+
export function SettingsMenu({ controller, onClose, initialTab, }) {
|
|
88
|
+
const tabs = [
|
|
89
|
+
...(controller.actions.length > 0 ? [{ id: "game", label: "Game", category: null }] : []),
|
|
90
|
+
...controller.categories.map((category) => ({ id: category.id, label: category.label, category })),
|
|
91
|
+
];
|
|
92
|
+
const [activeId, setActiveId] = useState(initialTab !== undefined && tabs.some((tab) => tab.id === initialTab) ? initialTab : (tabs[0]?.id ?? ""));
|
|
93
|
+
const [capturing, setCapturing] = useState(null);
|
|
94
|
+
const active = tabs.find((tab) => tab.id === activeId) ?? tabs[0] ?? null;
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (capturing === null)
|
|
97
|
+
return;
|
|
98
|
+
const onKey = (event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
event.stopPropagation();
|
|
101
|
+
if (event.code === "Escape") {
|
|
102
|
+
setCapturing(null);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
capturing.rebind(event.code);
|
|
106
|
+
setCapturing(null);
|
|
107
|
+
};
|
|
108
|
+
const onPointer = (event) => {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
capturing.rebind(`mouse${event.button}`);
|
|
112
|
+
setCapturing(null);
|
|
113
|
+
};
|
|
114
|
+
const onContext = (event) => event.preventDefault();
|
|
115
|
+
window.addEventListener("keydown", onKey, true);
|
|
116
|
+
window.addEventListener("pointerdown", onPointer, true);
|
|
117
|
+
window.addEventListener("contextmenu", onContext, true);
|
|
118
|
+
return () => {
|
|
119
|
+
window.removeEventListener("keydown", onKey, true);
|
|
120
|
+
window.removeEventListener("pointerdown", onPointer, true);
|
|
121
|
+
window.removeEventListener("contextmenu", onContext, true);
|
|
122
|
+
};
|
|
123
|
+
}, [capturing]);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (capturing !== null)
|
|
126
|
+
return;
|
|
127
|
+
const onKey = (event) => {
|
|
128
|
+
if (event.code === "Escape")
|
|
129
|
+
onClose();
|
|
130
|
+
};
|
|
131
|
+
window.addEventListener("keydown", onKey);
|
|
132
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
133
|
+
}, [capturing, onClose]);
|
|
134
|
+
if (active === null)
|
|
135
|
+
return null;
|
|
136
|
+
const bodyProps = { tab: active, actions: controller.actions, capturing, setCapturing, onClose };
|
|
137
|
+
const title = (_jsx("h2", { className: "text-base font-semibold tracking-wide", style: { color: text }, children: "Settings" }));
|
|
138
|
+
if (controller.variant === "fullscreen") {
|
|
139
|
+
return (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-40 flex flex-col", style: { background: surface, color: text, fontFamily: fontDisplay }, children: _jsxs("div", { className: "mx-auto flex h-full w-full max-w-2xl flex-col px-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 pb-4 pt-6", children: [_jsx("h2", { className: "text-2xl font-bold tracking-wide", style: { color: text }, children: "Settings" }), _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto pb-4", children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
|
|
140
|
+
}
|
|
141
|
+
if (controller.variant === "sidebar") {
|
|
142
|
+
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 })] })] }) }));
|
|
143
|
+
}
|
|
144
|
+
if (controller.variant === "sheet") {
|
|
145
|
+
return (_jsx(Backdrop, { onClose: onClose, align: "items-end", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-t-3xl shadow-2xl", style: { ...panelStyle, width: "min(100%, 640px)", height: "min(84vh, 620px)" }, children: [_jsx("div", { className: "flex flex-col items-center pt-2.5", children: _jsx("span", { className: "h-1.5 w-10 rounded-full", style: { background: "rgba(255,255,255,0.25)" } }) }), _jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto px-4 pb-3", children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx("div", { style: { borderTop: `1px solid ${edge}` }, className: "flex flex-1 flex-col overflow-hidden", children: _jsx(Body, { ...bodyProps }) })] }) }));
|
|
146
|
+
}
|
|
147
|
+
return (_jsx(Backdrop, { onClose: onClose, align: "items-center", children: _jsxs("div", { onClick: stop, className: "flex flex-col overflow-hidden rounded-2xl shadow-2xl", style: { ...panelStyle, width: "min(92vw, 460px)", height: "min(86vh, 580px)" }, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3.5", style: { borderBottom: `1px solid ${edge}` }, children: [title, _jsx(CloseButton, { onClose: onClose })] }), _jsx("div", { className: "flex gap-1.5 overflow-x-auto px-4 py-3", style: { borderBottom: `1px solid ${edge}` }, children: tabs.map((tab) => (_jsx(TabButton, { tab: tab, selected: tab.id === active.id, orientation: "row", onSelect: () => setActiveId(tab.id) }, tab.id))) }), _jsx(Body, { ...bodyProps })] }) }));
|
|
148
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { SettingsSurface, SettingsVariant } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { type SettingsActionView } from "@jgengine/react/settings";
|
|
4
|
+
import { type SettingsControllerInput } from "./settingsController.js";
|
|
5
|
+
export interface SettingsRuntimeProps extends SettingsControllerInput {
|
|
6
|
+
variant: SettingsVariant;
|
|
7
|
+
surface: SettingsSurface | false;
|
|
8
|
+
actions: readonly SettingsActionView[];
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare function SettingsRuntime({ variant, surface, actions, children, ...input }: SettingsRuntimeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { SettingsControllerProvider, } from "@jgengine/react/settings";
|
|
4
|
+
import { useSettingsCategories } from "./settingsController.js";
|
|
5
|
+
export function SettingsRuntime({ variant, surface, actions, children, ...input }) {
|
|
6
|
+
const categories = useSettingsCategories(input);
|
|
7
|
+
const [isOpen, setOpen] = useState(false);
|
|
8
|
+
const controller = useMemo(() => ({
|
|
9
|
+
categories,
|
|
10
|
+
actions: [...actions],
|
|
11
|
+
variant,
|
|
12
|
+
surface,
|
|
13
|
+
isOpen,
|
|
14
|
+
open: () => setOpen(true),
|
|
15
|
+
close: () => setOpen(false),
|
|
16
|
+
setOpen,
|
|
17
|
+
}), [categories, actions, variant, surface, isOpen]);
|
|
18
|
+
return _jsx(SettingsControllerProvider, { controller: controller, children: children });
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
4
|
+
export declare function useSettingsRevision(store: SettingsStore): number;
|
|
5
|
+
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
|
|
6
|
+
shadows: boolean;
|
|
7
|
+
dpr: number;
|
|
8
|
+
uiScale: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function AudioSettingsBridge({ store, engine, buses, }: {
|
|
11
|
+
store: SettingsStore;
|
|
12
|
+
engine: AudioEngine;
|
|
13
|
+
buses: Record<string, AudioBusDef> | undefined;
|
|
14
|
+
}): null;
|