@jgengine/shell 0.8.0 → 0.10.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 +36 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +10 -22
- package/dist/GamePlayerShell.js +709 -264
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +9 -0
- package/dist/audio/audioEngine.js +37 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/behaviour.d.ts +5 -24
- package/dist/behaviour.js +14 -50
- 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.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +35 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +44 -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 +3 -0
- package/dist/camera/cameraRigs.js +36 -33
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +3 -0
- package/dist/camera/index.js +3 -0
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/camera/rigMath.d.ts +10 -0
- package/dist/camera/rigMath.js +36 -1
- 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 +246 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +4 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +181 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
- package/dist/devtools/DevtoolsOverlay.js +383 -41
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +103 -0
- package/dist/devtools/collisionDebugMath.js +129 -0
- package/dist/environment/Daylight.d.ts +20 -8
- package/dist/environment/Daylight.js +63 -15
- package/dist/environment/EnvironmentScene.js +91 -35
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/groundPadMath.d.ts +2 -2
- package/dist/environment/groundPadMath.js +1 -1
- package/dist/environment/index.d.ts +2 -1
- package/dist/environment/index.js +1 -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/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/multiplayer.js +7 -3
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -0
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +82 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +64 -0
- package/dist/render/modelRender.d.ts +10 -1
- package/dist/render/modelRender.js +32 -5
- 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 +5 -1
- package/dist/terrain/CarvedTerrain.js +6 -5
- 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/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/terrain/terrainMath.d.ts +8 -0
- package/dist/terrain/terrainMath.js +9 -0
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +17 -1
- package/dist/touch/TouchControlsOverlay.js +125 -9
- 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/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 +1 -1
- package/dist/world/DataObjects.js +3 -1
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +6 -1
- package/dist/world/WorldHud.js +95 -48
- 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 +57 -0
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +1522 -1143
- package/package.json +4 -4
|
@@ -4,11 +4,86 @@ import { createGestureSurfaceTracker } from "@jgengine/core/input/gestureSurface
|
|
|
4
4
|
import { createTouchGestureTracker } from "@jgengine/core/input/touchGestures";
|
|
5
5
|
import { touchCode } from "@jgengine/core/input/touchScheme";
|
|
6
6
|
import { GameIcon, iconForAction, isGameIconName } from "@jgengine/react/gameIcons";
|
|
7
|
+
import { useRegisterLayoutRegion } from "@jgengine/react/gameViewport";
|
|
8
|
+
const MOVEMENT_ZONE = { id: "control:movement", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
|
|
9
|
+
const ACTIONS_ZONE = { id: "control:actions", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
|
|
10
|
+
const UTILITY_ZONE = { id: "control:utility", kind: "control", collisionPolicy: "warn", collisionGroup: "touch-dock" };
|
|
7
11
|
const JOYSTICK_SIZE = 132;
|
|
8
12
|
const JOYSTICK_THUMB = 52;
|
|
9
13
|
const JOYSTICK_DEADZONE = 0.28;
|
|
10
14
|
const JOYSTICK_AXIS_THRESHOLD = 0.42;
|
|
11
15
|
const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
|
|
16
|
+
const BUTTON_SIZE = 56;
|
|
17
|
+
const RING1_RADIUS = 112;
|
|
18
|
+
const RING2_RADIUS = 178;
|
|
19
|
+
const RING1_CAPACITY = 3;
|
|
20
|
+
const RING2_CAPACITY = 5;
|
|
21
|
+
const RING1_ANGLES = [[], [40], [16, 64], [12, 44, 76]];
|
|
22
|
+
const DOCK_BASE_PADDING = 20;
|
|
23
|
+
const UTILITY_ROW_HEIGHT = 28;
|
|
24
|
+
function polar(radius, deg, buttonSize) {
|
|
25
|
+
const rad = (deg * Math.PI) / 180;
|
|
26
|
+
return {
|
|
27
|
+
right: radius * Math.cos(rad) - buttonSize / 2,
|
|
28
|
+
bottom: radius * Math.sin(rad) - buttonSize / 2,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function ringAngles(count, from, to) {
|
|
32
|
+
if (count === 1)
|
|
33
|
+
return [(from + to) / 2];
|
|
34
|
+
const step = (to - from) / (count - 1);
|
|
35
|
+
return Array.from({ length: count }, (_, index) => from + index * step);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Thumb-arc placement for primary buttons around the bottom-right corner:
|
|
39
|
+
* up to three on an inner ring, the rest on an outer ring. Null means too
|
|
40
|
+
* many buttons for an arc — the dock falls back to a wrapping grid.
|
|
41
|
+
*/
|
|
42
|
+
export function primaryButtonOffsets(count, scale = 1) {
|
|
43
|
+
if (count === 0)
|
|
44
|
+
return [];
|
|
45
|
+
if (count > RING1_CAPACITY + RING2_CAPACITY)
|
|
46
|
+
return null;
|
|
47
|
+
const button = BUTTON_SIZE * scale;
|
|
48
|
+
const ring1 = RING1_ANGLES[Math.min(count, RING1_CAPACITY)] ?? [];
|
|
49
|
+
const offsets = ring1.map((angle) => polar(RING1_RADIUS * scale, angle, button));
|
|
50
|
+
const rest = count - ring1.length;
|
|
51
|
+
if (rest > 0) {
|
|
52
|
+
for (const angle of ringAngles(rest, 10, 80))
|
|
53
|
+
offsets.push(polar(RING2_RADIUS * scale, angle, button));
|
|
54
|
+
}
|
|
55
|
+
return offsets;
|
|
56
|
+
}
|
|
57
|
+
function primaryClusterExtent(count, scale = 1) {
|
|
58
|
+
if (count === 0)
|
|
59
|
+
return 0;
|
|
60
|
+
if (count <= RING1_CAPACITY)
|
|
61
|
+
return (RING1_RADIUS + BUTTON_SIZE / 2) * scale;
|
|
62
|
+
if (count <= RING1_CAPACITY + RING2_CAPACITY)
|
|
63
|
+
return (RING2_RADIUS + BUTTON_SIZE / 2) * scale;
|
|
64
|
+
return Math.ceil(count / 4) * (BUTTON_SIZE + 12) * scale;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Vertical space (px, excluding device safe areas) the dock occupies above
|
|
68
|
+
* the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so
|
|
69
|
+
* `HudCanvas` regions never collide with touch controls.
|
|
70
|
+
*/
|
|
71
|
+
export function touchDockClearance(scheme, scale = 1) {
|
|
72
|
+
if (scheme === null)
|
|
73
|
+
return 0;
|
|
74
|
+
let primary = 0;
|
|
75
|
+
let utility = 0;
|
|
76
|
+
for (const button of scheme.buttons) {
|
|
77
|
+
if (button.kind === "utility")
|
|
78
|
+
utility += 1;
|
|
79
|
+
else
|
|
80
|
+
primary += 1;
|
|
81
|
+
}
|
|
82
|
+
const tallest = Math.max(scheme.joystick !== null ? JOYSTICK_SIZE * scale : 0, primaryClusterExtent(primary, scale), utility > 0 ? UTILITY_ROW_HEIGHT : 0);
|
|
83
|
+
if (tallest === 0)
|
|
84
|
+
return 0;
|
|
85
|
+
return tallest + DOCK_BASE_PADDING + 12;
|
|
86
|
+
}
|
|
12
87
|
function pressOnce(sink, action) {
|
|
13
88
|
const code = touchCode(action);
|
|
14
89
|
sink.onCodeDown(code);
|
|
@@ -83,11 +158,13 @@ function joystickDirections(joystick, nx, ny) {
|
|
|
83
158
|
active.add(joystick.right);
|
|
84
159
|
return active;
|
|
85
160
|
}
|
|
86
|
-
function VirtualJoystick({ joystick, sink }) {
|
|
161
|
+
function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
87
162
|
const baseRef = useRef(null);
|
|
88
163
|
const thumbRef = useRef(null);
|
|
89
164
|
const pointerIdRef = useRef(null);
|
|
90
165
|
const activeRef = useRef(new Set());
|
|
166
|
+
const size = JOYSTICK_SIZE * scale;
|
|
167
|
+
const thumbSize = JOYSTICK_THUMB * scale;
|
|
91
168
|
const applyVector = (clientX, clientY) => {
|
|
92
169
|
const base = baseRef.current;
|
|
93
170
|
const thumb = thumbRef.current;
|
|
@@ -102,7 +179,7 @@ function VirtualJoystick({ joystick, sink }) {
|
|
|
102
179
|
nx /= length;
|
|
103
180
|
ny /= length;
|
|
104
181
|
}
|
|
105
|
-
const travel = radius -
|
|
182
|
+
const travel = radius - thumbSize / 2;
|
|
106
183
|
thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
|
|
107
184
|
const next = joystickDirections(joystick, nx, ny);
|
|
108
185
|
for (const action of activeRef.current) {
|
|
@@ -123,7 +200,7 @@ function VirtualJoystick({ joystick, sink }) {
|
|
|
123
200
|
if (thumbRef.current !== null)
|
|
124
201
|
thumbRef.current.style.transform = "translate(0px, 0px)";
|
|
125
202
|
};
|
|
126
|
-
return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full border border-white/20 bg-white/10 backdrop-blur-sm", style: { width:
|
|
203
|
+
return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full border border-white/20 bg-white/10 backdrop-blur-sm", style: { width: size, height: size }, onPointerDown: (event) => {
|
|
127
204
|
if (pointerIdRef.current !== null)
|
|
128
205
|
return;
|
|
129
206
|
pointerIdRef.current = event.pointerId;
|
|
@@ -139,7 +216,7 @@ function VirtualJoystick({ joystick, sink }) {
|
|
|
139
216
|
return;
|
|
140
217
|
event.stopPropagation();
|
|
141
218
|
releaseAll();
|
|
142
|
-
}, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full border border-white/30 bg-white/25", style: { width:
|
|
219
|
+
}, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full border border-white/30 bg-white/25", style: { width: thumbSize, height: thumbSize } }) }));
|
|
143
220
|
}
|
|
144
221
|
function touchButtonIcon(button) {
|
|
145
222
|
if (button.icon === false)
|
|
@@ -148,16 +225,36 @@ function touchButtonIcon(button) {
|
|
|
148
225
|
return button.icon;
|
|
149
226
|
return iconForAction(button.action);
|
|
150
227
|
}
|
|
151
|
-
function TouchActionButton({ button, sink }) {
|
|
228
|
+
function TouchActionButton({ button, sink, scale = 1 }) {
|
|
152
229
|
const pointerIdRef = useRef(null);
|
|
153
230
|
const icon = touchButtonIcon(button);
|
|
231
|
+
const size = BUTTON_SIZE * scale;
|
|
232
|
+
const releaseIfHeld = () => {
|
|
233
|
+
if (pointerIdRef.current === null)
|
|
234
|
+
return;
|
|
235
|
+
pointerIdRef.current = null;
|
|
236
|
+
sink.onCodeUp(touchCode(button.action));
|
|
237
|
+
};
|
|
238
|
+
return (_jsx("button", { type: "button", "aria-label": button.label, style: { width: size, height: size }, className: "pointer-events-auto flex touch-none select-none items-center justify-center rounded-full border border-white/25 bg-white/10 text-center text-[10px] font-semibold uppercase leading-tight tracking-wide text-white/90 backdrop-blur-sm active:border-white/50 active:bg-white/30", onPointerDown: (event) => {
|
|
239
|
+
if (pointerIdRef.current !== null)
|
|
240
|
+
return;
|
|
241
|
+
pointerIdRef.current = event.pointerId;
|
|
242
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
243
|
+
sink.onCodeDown(touchCode(button.action));
|
|
244
|
+
}, onPointerUp: (event) => {
|
|
245
|
+
event.stopPropagation();
|
|
246
|
+
releaseIfHeld();
|
|
247
|
+
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: icon !== null ? _jsx(GameIcon, { name: icon, size: Math.round(26 * scale) }) : _jsx("span", { className: "px-1", children: button.label }) }));
|
|
248
|
+
}
|
|
249
|
+
function TouchUtilityChip({ button, sink }) {
|
|
250
|
+
const pointerIdRef = useRef(null);
|
|
154
251
|
const releaseIfHeld = () => {
|
|
155
252
|
if (pointerIdRef.current === null)
|
|
156
253
|
return;
|
|
157
254
|
pointerIdRef.current = null;
|
|
158
255
|
sink.onCodeUp(touchCode(button.action));
|
|
159
256
|
};
|
|
160
|
-
return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-
|
|
257
|
+
return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-7 touch-none select-none items-center rounded-full border border-white/20 bg-black/40 px-3 text-[10px] font-semibold uppercase tracking-wide text-white/80 backdrop-blur-sm active:border-white/50 active:bg-white/25", onPointerDown: (event) => {
|
|
161
258
|
if (pointerIdRef.current !== null)
|
|
162
259
|
return;
|
|
163
260
|
pointerIdRef.current = event.pointerId;
|
|
@@ -166,8 +263,27 @@ function TouchActionButton({ button, sink }) {
|
|
|
166
263
|
}, onPointerUp: (event) => {
|
|
167
264
|
event.stopPropagation();
|
|
168
265
|
releaseIfHeld();
|
|
169
|
-
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children:
|
|
266
|
+
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: button.label }));
|
|
267
|
+
}
|
|
268
|
+
function PrimaryButtonCluster({ buttons, sink, scale = 1 }) {
|
|
269
|
+
const offsets = primaryButtonOffsets(buttons.length, scale);
|
|
270
|
+
if (offsets === null) {
|
|
271
|
+
return (_jsx("div", { className: "flex max-w-[60%] flex-wrap items-end justify-end gap-3", children: buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink, scale: scale }, button.action))) }));
|
|
272
|
+
}
|
|
273
|
+
const extent = primaryClusterExtent(buttons.length, scale);
|
|
274
|
+
return (_jsx("div", { className: "pointer-events-none relative", style: { width: extent, height: extent }, children: buttons.map((button, index) => {
|
|
275
|
+
const offset = offsets[index];
|
|
276
|
+
return (_jsx("div", { className: "absolute", style: { right: offset?.right ?? 0, bottom: offset?.bottom ?? 0 }, children: _jsx(TouchActionButton, { button: button, sink: sink, scale: scale }) }, button.action));
|
|
277
|
+
}) }));
|
|
170
278
|
}
|
|
171
|
-
export function TouchControlsDock({ scheme, sink }) {
|
|
172
|
-
|
|
279
|
+
export function TouchControlsDock({ scheme, sink, scale = 1 }) {
|
|
280
|
+
const primary = scheme.buttons.filter((button) => button.kind !== "utility");
|
|
281
|
+
const utility = scheme.buttons.filter((button) => button.kind === "utility");
|
|
282
|
+
const joystickRef = useRef(null);
|
|
283
|
+
const actionsRef = useRef(null);
|
|
284
|
+
const utilityRef = useRef(null);
|
|
285
|
+
useRegisterLayoutRegion(MOVEMENT_ZONE, joystickRef, scheme.joystick !== null);
|
|
286
|
+
useRegisterLayoutRegion(ACTIONS_ZONE, actionsRef, primary.length > 0);
|
|
287
|
+
useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
|
|
288
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 z-40", style: { paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${DOCK_BASE_PADDING}px)` }, children: [_jsxs("div", { className: "flex items-end justify-between gap-4 px-5", children: [_jsx("div", { ref: joystickRef, children: scheme.joystick !== null ? _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, scale: scale }) : null }), _jsx("div", { ref: actionsRef, children: _jsx(PrimaryButtonCluster, { buttons: primary, sink: sink, scale: scale }) })] }), utility.length > 0 ? (_jsx("div", { className: "pointer-events-none absolute inset-x-0 flex justify-center", style: { bottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)" }, children: _jsx("div", { ref: utilityRef, className: "flex gap-2", children: utility.map((button) => (_jsx(TouchUtilityChip, { button: button, sink: sink }, button.action))) }) })) : null] }));
|
|
173
289
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode, type MutableRefObject } from "react";
|
|
2
|
+
import type { VisibilityConfig } from "@jgengine/core/visibility/config";
|
|
3
|
+
type VisiblePredicate = (id: string) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Read the current render-visibility predicate. Backward compatible: with no CullingProvider
|
|
6
|
+
* mounted (or culling disabled) it returns an always-visible ref, so a marker that consults it
|
|
7
|
+
* behaves exactly as before this feature existed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useRenderVisibility(): MutableRefObject<VisiblePredicate>;
|
|
10
|
+
/**
|
|
11
|
+
* Drives automatic frustum + distance culling for every entity and placed object. It reads the
|
|
12
|
+
* live render camera each frame, updates the engine VisibilitySystem, and exposes a predicate the
|
|
13
|
+
* entity/object markers consult to toggle `group.visible` — objects fully outside the view (plus a
|
|
14
|
+
* conservative preload margin) are never submitted to the renderer, without unmounting them or
|
|
15
|
+
* touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
16
|
+
*/
|
|
17
|
+
export declare function CullingProvider({ config, children }: {
|
|
18
|
+
config: VisibilityConfig | undefined;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo, useRef } from "react";
|
|
3
|
+
import { useThree, useFrame } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
|
|
7
|
+
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/index.js";
|
|
8
|
+
const ALWAYS_VISIBLE = () => true;
|
|
9
|
+
const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
|
|
10
|
+
const CullingContext = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* Read the current render-visibility predicate. Backward compatible: with no CullingProvider
|
|
13
|
+
* mounted (or culling disabled) it returns an always-visible ref, so a marker that consults it
|
|
14
|
+
* behaves exactly as before this feature existed.
|
|
15
|
+
*/
|
|
16
|
+
export function useRenderVisibility() {
|
|
17
|
+
return useContext(CullingContext) ?? alwaysVisibleRef;
|
|
18
|
+
}
|
|
19
|
+
const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
|
|
20
|
+
function objectRadius(scale) {
|
|
21
|
+
if (scale === undefined)
|
|
22
|
+
return 1;
|
|
23
|
+
const s = typeof scale === "number" ? scale : Math.max(scale[0], scale[1], scale[2]);
|
|
24
|
+
return Math.max(0.75, s);
|
|
25
|
+
}
|
|
26
|
+
function buildSystem(ctx, config) {
|
|
27
|
+
const versions = new Map();
|
|
28
|
+
let currentView = null;
|
|
29
|
+
const scratch = { id: "", position: [0, 0, 0], version: 0 };
|
|
30
|
+
const objectSpec = { kind: "sphere", radius: 1, offset: [0, 0.5, 0] };
|
|
31
|
+
function bump(id, position, rot) {
|
|
32
|
+
const prev = versions.get(id);
|
|
33
|
+
if (prev !== undefined && prev.x === position[0] && prev.y === position[1] && prev.z === position[2] && prev.rot === rot) {
|
|
34
|
+
return prev.version;
|
|
35
|
+
}
|
|
36
|
+
const version = (prev?.version ?? 0) + 1;
|
|
37
|
+
versions.set(id, { x: position[0], y: position[1], z: position[2], rot, version });
|
|
38
|
+
return version;
|
|
39
|
+
}
|
|
40
|
+
function* renderables() {
|
|
41
|
+
const entityOverrides = config?.entities;
|
|
42
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
43
|
+
scratch.id = entity.id;
|
|
44
|
+
scratch.position = entity.position;
|
|
45
|
+
scratch.version = bump(entity.id, entity.position, entity.rotationY);
|
|
46
|
+
scratch.bounds = ENTITY_BOUNDS;
|
|
47
|
+
scratch.overrides = entityOverrides?.[entity.name];
|
|
48
|
+
scratch.layer = "entity";
|
|
49
|
+
yield scratch;
|
|
50
|
+
}
|
|
51
|
+
const objectOverrides = config?.objects;
|
|
52
|
+
for (const object of ctx.scene.object.list()) {
|
|
53
|
+
scratch.id = object.instanceId;
|
|
54
|
+
scratch.position = object.position;
|
|
55
|
+
scratch.version = bump(object.instanceId, object.position, object.rotationY);
|
|
56
|
+
objectSpec.radius = objectRadius(object.visual?.scale);
|
|
57
|
+
scratch.bounds = objectSpec;
|
|
58
|
+
scratch.overrides = objectOverrides?.[object.catalogId];
|
|
59
|
+
scratch.layer = "object";
|
|
60
|
+
yield scratch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const cameras = () => (currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }]);
|
|
64
|
+
const options = {
|
|
65
|
+
renderables,
|
|
66
|
+
cameras,
|
|
67
|
+
...(config?.culling !== undefined ? { settings: config.culling } : {}),
|
|
68
|
+
...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
|
|
69
|
+
};
|
|
70
|
+
const system = createVisibilitySystem(options);
|
|
71
|
+
return {
|
|
72
|
+
system,
|
|
73
|
+
setView(view) {
|
|
74
|
+
currentView = view;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const EMPTY_CAMERAS = [];
|
|
79
|
+
const tmpDir = new THREE.Vector3();
|
|
80
|
+
function viewFromCamera(camera) {
|
|
81
|
+
camera.getWorldDirection(tmpDir);
|
|
82
|
+
const p = camera.position;
|
|
83
|
+
const position = [p.x, p.y, p.z];
|
|
84
|
+
const target = [p.x + tmpDir.x, p.y + tmpDir.y, p.z + tmpDir.z];
|
|
85
|
+
const up = [camera.up.x, camera.up.y, camera.up.z];
|
|
86
|
+
if (camera.isPerspectiveCamera) {
|
|
87
|
+
const c = camera;
|
|
88
|
+
return { kind: "perspective", position, target, up, fovDeg: c.fov, aspect: c.aspect, near: c.near, far: c.far };
|
|
89
|
+
}
|
|
90
|
+
if (camera.isOrthographicCamera) {
|
|
91
|
+
const c = camera;
|
|
92
|
+
const zoom = c.zoom || 1;
|
|
93
|
+
return {
|
|
94
|
+
kind: "orthographic",
|
|
95
|
+
position,
|
|
96
|
+
target,
|
|
97
|
+
up,
|
|
98
|
+
halfWidth: Math.abs((c.right - c.left) / 2 / zoom),
|
|
99
|
+
halfHeight: Math.abs((c.top - c.bottom) / 2 / zoom),
|
|
100
|
+
near: c.near,
|
|
101
|
+
far: c.far,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Drives automatic frustum + distance culling for every entity and placed object. It reads the
|
|
108
|
+
* live render camera each frame, updates the engine VisibilitySystem, and exposes a predicate the
|
|
109
|
+
* entity/object markers consult to toggle `group.visible` — objects fully outside the view (plus a
|
|
110
|
+
* conservative preload margin) are never submitted to the renderer, without unmounting them or
|
|
111
|
+
* touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
112
|
+
*/
|
|
113
|
+
export function CullingProvider({ config, children }) {
|
|
114
|
+
const ctx = useGameContext();
|
|
115
|
+
const enabled = config?.enabled !== false;
|
|
116
|
+
const predicateRef = useRef(ALWAYS_VISIBLE);
|
|
117
|
+
const camera = useThree((state) => state.camera);
|
|
118
|
+
const driver = useMemo(() => (enabled ? buildSystem(ctx, config) : null), [ctx, config, enabled]);
|
|
119
|
+
useFrame(() => {
|
|
120
|
+
if (driver === null) {
|
|
121
|
+
predicateRef.current = ALWAYS_VISIBLE;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const view = viewFromCamera(camera);
|
|
125
|
+
if (view === null) {
|
|
126
|
+
predicateRef.current = ALWAYS_VISIBLE;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
driver.setView(view);
|
|
130
|
+
const visible = driver.system.update().visible;
|
|
131
|
+
predicateRef.current = (id) => visible.has(id);
|
|
132
|
+
}, CAMERA_POST_FRAME_PRIORITY);
|
|
133
|
+
return _jsx(CullingContext.Provider, { value: predicateRef, children: children });
|
|
134
|
+
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { type FramingConfig, type FrustumSample } from "@jgengine/core/sensor/frustumSensor";
|
|
2
|
+
export { frustumSampleDisplayEqual } from "./frustumSampleEqual.js";
|
|
2
3
|
export interface FrustumSensorProbeOptions extends FramingConfig {
|
|
3
4
|
subjectIds: readonly string[];
|
|
4
5
|
subjectRadius?: number;
|
|
5
6
|
}
|
|
6
|
-
/**
|
|
7
|
-
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
8
|
-
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
9
|
-
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
10
|
-
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
11
|
-
*/
|
|
12
7
|
export declare function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null;
|
|
13
8
|
/**
|
|
14
9
|
* Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`)
|
|
@@ -5,21 +5,18 @@ import { useMemo, useRef, useState } from "react";
|
|
|
5
5
|
import { PerspectiveCamera, Vector3 } from "three";
|
|
6
6
|
import { createFrustumSensor, } from "@jgengine/core/sensor/frustumSensor";
|
|
7
7
|
import { useSceneEntities } from "@jgengine/react/hooks";
|
|
8
|
+
import { frustumSampleDisplayEqual } from "./frustumSampleEqual.js";
|
|
9
|
+
export { frustumSampleDisplayEqual } from "./frustumSampleEqual.js";
|
|
8
10
|
function isPerspective(camera) {
|
|
9
11
|
return camera.isPerspectiveCamera === true;
|
|
10
12
|
}
|
|
11
|
-
/**
|
|
12
|
-
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
13
|
-
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
14
|
-
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
15
|
-
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
16
|
-
*/
|
|
17
13
|
export function useFrustumSensor(options) {
|
|
18
14
|
const camera = useThree((state) => state.camera);
|
|
19
15
|
const size = useThree((state) => state.size);
|
|
20
16
|
const entities = useSceneEntities();
|
|
21
17
|
const sensor = useMemo(() => createFrustumSensor(options), []);
|
|
22
18
|
const [best, setBest] = useState(null);
|
|
19
|
+
const bestRef = useRef(null);
|
|
23
20
|
const forward = useRef(new Vector3());
|
|
24
21
|
useFrame((_, dt) => {
|
|
25
22
|
const targets = [];
|
|
@@ -29,28 +26,29 @@ export function useFrustumSensor(options) {
|
|
|
29
26
|
continue;
|
|
30
27
|
targets.push({ id, position: entity.position, radius: options.subjectRadius });
|
|
31
28
|
}
|
|
32
|
-
if (targets.length === 0) {
|
|
33
|
-
setBest(null);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
camera.getWorldDirection(forward.current);
|
|
37
|
-
const samples = sensor.tick({
|
|
38
|
-
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
39
|
-
lookAt: [
|
|
40
|
-
camera.position.x + forward.current.x,
|
|
41
|
-
camera.position.y + forward.current.y,
|
|
42
|
-
camera.position.z + forward.current.z,
|
|
43
|
-
],
|
|
44
|
-
fovDeg: isPerspective(camera) ? camera.fov : 55,
|
|
45
|
-
aspect: size.height === 0 ? 16 / 9 : size.width / size.height,
|
|
46
|
-
}, targets, dt);
|
|
47
29
|
let leader = null;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
if (targets.length > 0) {
|
|
31
|
+
camera.getWorldDirection(forward.current);
|
|
32
|
+
const samples = sensor.tick({
|
|
33
|
+
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
34
|
+
lookAt: [
|
|
35
|
+
camera.position.x + forward.current.x,
|
|
36
|
+
camera.position.y + forward.current.y,
|
|
37
|
+
camera.position.z + forward.current.z,
|
|
38
|
+
],
|
|
39
|
+
fovDeg: isPerspective(camera) ? camera.fov : 55,
|
|
40
|
+
aspect: size.height === 0 ? 16 / 9 : size.width / size.height,
|
|
41
|
+
}, targets, dt);
|
|
42
|
+
for (const sample of samples) {
|
|
43
|
+
if (!sample.inView)
|
|
44
|
+
continue;
|
|
45
|
+
if (leader === null || sample.framing > leader.framing)
|
|
46
|
+
leader = sample;
|
|
47
|
+
}
|
|
53
48
|
}
|
|
49
|
+
if (frustumSampleDisplayEqual(bestRef.current, leader))
|
|
50
|
+
return;
|
|
51
|
+
bestRef.current = leader;
|
|
54
52
|
setBest(leader);
|
|
55
53
|
});
|
|
56
54
|
return best;
|
|
@@ -62,6 +60,6 @@ export function useFrustumSensor(options) {
|
|
|
62
60
|
*/
|
|
63
61
|
export function FrustumSensorReadout(props) {
|
|
64
62
|
const sample = useFrustumSensor(props);
|
|
65
|
-
return (_jsx(Html, { fullscreen: true, children: _jsx("div", { className: props.wrapperClassName ?? "pointer-events-none absolute bottom-24 right-4", children: sample === null ? (_jsx("div", { className: props.className ?? "rounded border border-white/15 bg-black/60 px-3 py-1.5 text-xs text-white/50", children: "No subject framed" })) : (_jsxs("div", { className: props.className ??
|
|
63
|
+
return (_jsx(Html, { fullscreen: true, calculatePosition: (_el, _camera, size) => [size.width / 2, size.height / 2], children: _jsx("div", { className: props.wrapperClassName ?? "pointer-events-none absolute bottom-24 right-4", children: sample === null ? (_jsx("div", { className: props.className ?? "rounded border border-white/15 bg-black/60 px-3 py-1.5 text-xs text-white/50", children: "No subject framed" })) : (_jsxs("div", { className: props.className ??
|
|
66
64
|
"rounded border border-amber-300/40 bg-black/70 px-3 py-1.5 text-xs text-amber-200 shadow-lg", children: [_jsx("span", { className: "font-semibold uppercase tracking-wide", children: sample.id }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["framing ", Math.round(sample.framing * 100), "%"] }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["dwell ", sample.dwellSeconds.toFixed(1), "s"] })] })) }) }));
|
|
67
65
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function frustumSampleDisplayEqual(a, b) {
|
|
2
|
+
if (a === b)
|
|
3
|
+
return true;
|
|
4
|
+
if (a === null || b === null)
|
|
5
|
+
return false;
|
|
6
|
+
return (a.id === b.id &&
|
|
7
|
+
a.inView === b.inView &&
|
|
8
|
+
Math.round(a.framing * 100) === Math.round(b.framing * 100) &&
|
|
9
|
+
Math.round(a.dwellSeconds * 10) === Math.round(b.dwellSeconds * 10));
|
|
10
|
+
}
|
package/dist/water/Ocean.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as THREE from "three";
|
|
|
5
5
|
import { createOceanConfig } from "./OceanConfig.js";
|
|
6
6
|
import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
|
|
7
7
|
function createOceanGeometry(config) {
|
|
8
|
-
const geometry = new THREE.PlaneGeometry(config.size, config.
|
|
8
|
+
const geometry = new THREE.PlaneGeometry(config.size, config.depth, config.resolution, config.resolution);
|
|
9
9
|
geometry.rotateX(-Math.PI / 2);
|
|
10
10
|
geometry.computeBoundingSphere();
|
|
11
11
|
return geometry;
|
|
@@ -30,7 +30,10 @@ export interface OceanFoamConfig {
|
|
|
30
30
|
}
|
|
31
31
|
export interface OceanConfig {
|
|
32
32
|
quality?: OceanQualityPreset;
|
|
33
|
+
/** Ocean plane width in world units (X). When `depth` is omitted, also used for depth (square). */
|
|
33
34
|
size?: number;
|
|
35
|
+
/** Ocean plane depth in world units (Z). Defaults to `size` for a square sheet. */
|
|
36
|
+
depth?: number;
|
|
34
37
|
resolution?: number;
|
|
35
38
|
amplitude?: number;
|
|
36
39
|
speed?: number;
|
|
@@ -38,6 +41,12 @@ export interface OceanConfig {
|
|
|
38
41
|
choppiness?: number;
|
|
39
42
|
steepness?: number;
|
|
40
43
|
timeScale?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Primary Gerstner wavelength in world units — same unit as `@jgengine/core/world/water` `waveScale`
|
|
46
|
+
* (default 18). Scales the default wave set so the longest wave matches this value; explicit `waves`
|
|
47
|
+
* wavelengths are multiplied by `waveScale / 18`.
|
|
48
|
+
*/
|
|
49
|
+
waveScale?: number;
|
|
41
50
|
color?: OceanColorConfig;
|
|
42
51
|
foam?: OceanFoamConfig;
|
|
43
52
|
waves?: readonly OceanWaveConfig[];
|
|
@@ -67,6 +76,7 @@ export interface ResolvedOceanWaveConfig {
|
|
|
67
76
|
export interface ResolvedOceanConfig {
|
|
68
77
|
quality: OceanQualityPreset;
|
|
69
78
|
size: number;
|
|
79
|
+
depth: number;
|
|
70
80
|
resolution: number;
|
|
71
81
|
amplitude: number;
|
|
72
82
|
speed: number;
|
|
@@ -74,10 +84,13 @@ export interface ResolvedOceanConfig {
|
|
|
74
84
|
choppiness: number;
|
|
75
85
|
steepness: number;
|
|
76
86
|
timeScale: number;
|
|
87
|
+
waveScale: number;
|
|
77
88
|
color: ResolvedOceanColorConfig;
|
|
78
89
|
foam: ResolvedOceanFoamConfig;
|
|
79
90
|
waves: readonly ResolvedOceanWaveConfig[];
|
|
80
91
|
}
|
|
92
|
+
/** Shared with `@jgengine/core/world/water` — primary wavelength in world units. */
|
|
93
|
+
export declare const DEFAULT_OCEAN_WAVE_SCALE = 18;
|
|
81
94
|
export declare const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, {
|
|
82
95
|
size: number;
|
|
83
96
|
resolution: number;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
export const MAX_OCEAN_WAVES = 6;
|
|
3
|
+
/** Shared with `@jgengine/core/world/water` — primary wavelength in world units. */
|
|
4
|
+
export const DEFAULT_OCEAN_WAVE_SCALE = 18;
|
|
3
5
|
export const OCEAN_QUALITY_PRESETS = {
|
|
4
6
|
low: { size: 220, resolution: 80 },
|
|
5
7
|
medium: { size: 360, resolution: 128 },
|
|
@@ -9,6 +11,7 @@ export const OCEAN_QUALITY_PRESETS = {
|
|
|
9
11
|
export const DEFAULT_OCEAN_CONFIG = {
|
|
10
12
|
quality: "medium",
|
|
11
13
|
size: OCEAN_QUALITY_PRESETS.medium.size,
|
|
14
|
+
depth: OCEAN_QUALITY_PRESETS.medium.size,
|
|
12
15
|
resolution: OCEAN_QUALITY_PRESETS.medium.resolution,
|
|
13
16
|
amplitude: 1,
|
|
14
17
|
speed: 1,
|
|
@@ -16,6 +19,7 @@ export const DEFAULT_OCEAN_CONFIG = {
|
|
|
16
19
|
choppiness: 1,
|
|
17
20
|
steepness: 0.72,
|
|
18
21
|
timeScale: 1,
|
|
22
|
+
waveScale: DEFAULT_OCEAN_WAVE_SCALE,
|
|
19
23
|
color: {
|
|
20
24
|
shallow: "#2dc5d3",
|
|
21
25
|
deep: "#064468",
|
|
@@ -33,14 +37,10 @@ export const DEFAULT_OCEAN_CONFIG = {
|
|
|
33
37
|
},
|
|
34
38
|
waves: [],
|
|
35
39
|
};
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{ amplitude: 0.28, wavelength: 10, speed: 1.34, steepness: 0.38 },
|
|
41
|
-
{ amplitude: 0.18, wavelength: 6.5, speed: 1.52, steepness: 0.28 },
|
|
42
|
-
{ amplitude: 0.11, wavelength: 3.8, speed: 1.75, steepness: 0.18 },
|
|
43
|
-
];
|
|
40
|
+
const WAVE_WAVELENGTH_FALLOFF = 0.65;
|
|
41
|
+
const WAVE_AMPLITUDE_SHAPE = [1.15, 0.72, 0.44, 0.28, 0.18, 0.11];
|
|
42
|
+
const WAVE_SPEED_SHAPE = [1, 1.08, 1.18, 1.34, 1.52, 1.75];
|
|
43
|
+
const WAVE_STEEPNESS_SHAPE = [0.68, 0.58, 0.48, 0.38, 0.28, 0.18];
|
|
44
44
|
const DEFAULT_DIRECTION_OFFSETS = [-8, 7, 21, -31, 46, -58];
|
|
45
45
|
function finiteOr(value, fallback) {
|
|
46
46
|
return value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
@@ -67,18 +67,21 @@ function normalizeDirection(direction, fallback) {
|
|
|
67
67
|
}
|
|
68
68
|
function createDefaultWaves(config) {
|
|
69
69
|
const baseAngle = directionAngle(config.direction);
|
|
70
|
-
return
|
|
71
|
-
amplitude
|
|
72
|
-
wavelength:
|
|
73
|
-
speed:
|
|
70
|
+
return WAVE_AMPLITUDE_SHAPE.map((amplitude, index) => ({
|
|
71
|
+
amplitude,
|
|
72
|
+
wavelength: Math.max(0.1, config.waveScale * WAVE_WAVELENGTH_FALLOFF ** index),
|
|
73
|
+
speed: WAVE_SPEED_SHAPE[index],
|
|
74
74
|
direction: directionFromAngle(baseAngle + DEFAULT_DIRECTION_OFFSETS[index]),
|
|
75
|
-
steepness:
|
|
75
|
+
steepness: WAVE_STEEPNESS_SHAPE[index],
|
|
76
76
|
}));
|
|
77
77
|
}
|
|
78
|
-
function resolveWave(wave, fallback, config) {
|
|
78
|
+
function resolveWave(wave, fallback, config, customWaves) {
|
|
79
|
+
const wavelength = customWaves && wave?.wavelength !== undefined
|
|
80
|
+
? wave.wavelength * (config.waveScale / DEFAULT_OCEAN_WAVE_SCALE)
|
|
81
|
+
: finiteOr(wave?.wavelength, fallback.wavelength);
|
|
79
82
|
return {
|
|
80
83
|
amplitude: Math.max(0, finiteOr(wave?.amplitude, fallback.amplitude) * config.amplitude),
|
|
81
|
-
wavelength: Math.max(0.1,
|
|
84
|
+
wavelength: Math.max(0.1, wavelength),
|
|
82
85
|
speed: finiteOr(wave?.speed, fallback.speed) * config.speed,
|
|
83
86
|
direction: normalizeDirection(wave?.direction, fallback.direction),
|
|
84
87
|
steepness: clamp(finiteOr(wave?.steepness, fallback.steepness) * config.steepness, 0, 1.2),
|
|
@@ -87,9 +90,12 @@ function resolveWave(wave, fallback, config) {
|
|
|
87
90
|
export function createOceanConfig(patch = {}) {
|
|
88
91
|
const quality = patch.quality ?? DEFAULT_OCEAN_CONFIG.quality;
|
|
89
92
|
const preset = OCEAN_QUALITY_PRESETS[quality];
|
|
93
|
+
const size = Math.max(1, finiteOr(patch.size, preset.size));
|
|
94
|
+
const depth = Math.max(1, finiteOr(patch.depth, size));
|
|
90
95
|
const withoutWaves = {
|
|
91
96
|
quality,
|
|
92
|
-
size
|
|
97
|
+
size,
|
|
98
|
+
depth,
|
|
93
99
|
resolution: Math.max(1, Math.floor(finiteOr(patch.resolution, preset.resolution))),
|
|
94
100
|
amplitude: Math.max(0, finiteOr(patch.amplitude, DEFAULT_OCEAN_CONFIG.amplitude)),
|
|
95
101
|
speed: finiteOr(patch.speed, DEFAULT_OCEAN_CONFIG.speed),
|
|
@@ -97,6 +103,7 @@ export function createOceanConfig(patch = {}) {
|
|
|
97
103
|
choppiness: Math.max(0, finiteOr(patch.choppiness, DEFAULT_OCEAN_CONFIG.choppiness)),
|
|
98
104
|
steepness: Math.max(0, finiteOr(patch.steepness, DEFAULT_OCEAN_CONFIG.steepness)),
|
|
99
105
|
timeScale: finiteOr(patch.timeScale, DEFAULT_OCEAN_CONFIG.timeScale),
|
|
106
|
+
waveScale: Math.max(0.1, finiteOr(patch.waveScale, DEFAULT_OCEAN_CONFIG.waveScale)),
|
|
100
107
|
color: {
|
|
101
108
|
...DEFAULT_OCEAN_CONFIG.color,
|
|
102
109
|
...patch.color,
|
|
@@ -114,8 +121,9 @@ export function createOceanConfig(patch = {}) {
|
|
|
114
121
|
},
|
|
115
122
|
};
|
|
116
123
|
const defaultWaves = createDefaultWaves(withoutWaves);
|
|
124
|
+
const customWaves = patch.waves !== undefined;
|
|
117
125
|
const inputWaves = patch.waves ?? defaultWaves;
|
|
118
|
-
const waves = Array.from({ length: MAX_OCEAN_WAVES }, (_, index) => resolveWave(inputWaves[index], defaultWaves[index], withoutWaves));
|
|
126
|
+
const waves = Array.from({ length: MAX_OCEAN_WAVES }, (_, index) => resolveWave(inputWaves[index], defaultWaves[index], withoutWaves, customWaves));
|
|
119
127
|
return { ...withoutWaves, waves };
|
|
120
128
|
}
|
|
121
129
|
export function buildOceanWaveUniforms(config) {
|
package/dist/water/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { Ocean, type OceanProps } from "./Ocean.js";
|
|
2
|
-
export { DEFAULT_OCEAN_CONFIG, MAX_OCEAN_WAVES, OCEAN_QUALITY_PRESETS, buildOceanWaveUniforms, createOceanConfig, type OceanColorConfig, type OceanConfig, type OceanDirectionVector, type OceanFoamConfig, type OceanQualityPreset, type OceanWaveConfig, type OceanWaveDirection, type ResolvedOceanColorConfig, type ResolvedOceanConfig, type ResolvedOceanFoamConfig, type ResolvedOceanWaveConfig, } from "./OceanConfig.js";
|
|
2
|
+
export { DEFAULT_OCEAN_CONFIG, MAX_OCEAN_WAVES, OCEAN_QUALITY_PRESETS, buildOceanWaveUniforms, createOceanConfig, DEFAULT_OCEAN_WAVE_SCALE, type OceanColorConfig, type OceanConfig, type OceanDirectionVector, type OceanFoamConfig, type OceanQualityPreset, type OceanWaveConfig, type OceanWaveDirection, type ResolvedOceanColorConfig, type ResolvedOceanConfig, type ResolvedOceanFoamConfig, type ResolvedOceanWaveConfig, } from "./OceanConfig.js";
|
|
3
3
|
export { createOceanMaterial, syncOceanMaterial, type OceanMaterialUniforms, type OceanShaderMaterial, } from "./OceanMaterial.js";
|
package/dist/water/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { Ocean } from "./Ocean.js";
|
|
2
|
-
export { DEFAULT_OCEAN_CONFIG, MAX_OCEAN_WAVES, OCEAN_QUALITY_PRESETS, buildOceanWaveUniforms, createOceanConfig, } from "./OceanConfig.js";
|
|
2
|
+
export { DEFAULT_OCEAN_CONFIG, MAX_OCEAN_WAVES, OCEAN_QUALITY_PRESETS, buildOceanWaveUniforms, createOceanConfig, DEFAULT_OCEAN_WAVE_SCALE, } from "./OceanConfig.js";
|
|
3
3
|
export { createOceanMaterial, syncOceanMaterial, } from "./OceanMaterial.js";
|