@vgai/engine 0.5.2 → 0.5.3
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/README.md +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical `
|
|
2
|
+
* Canonical `WorldProvider`/`useGame`/`useWorldState`/`useRootObservation`
|
|
3
3
|
* entry for React adapter roots. Also home to `useDebugProvider` and
|
|
4
4
|
* `useDebugCommand`/`useDebugEmit`, the React-facing debug seam.
|
|
5
5
|
*
|
|
6
6
|
* This is a SEPARATE, react-value-importing module under
|
|
7
|
-
* `packages/engine/src/react/` — colocated per
|
|
8
|
-
*
|
|
9
|
-
* core" rule, just now living IN the engine package rather than only in the
|
|
7
|
+
* `packages/engine/src/react/` — colocated per the "colocate the
|
|
8
|
+
* react-facing hooks with the HUD seam, not the react-free core" rule, just now living IN the engine package rather than only in the
|
|
10
9
|
* project template. It is imported by NOTHING in the engine core import
|
|
11
10
|
* graph (`packages/engine/src/` outside this `react/` directory) — that
|
|
12
11
|
* invariant is proved by `packages/engine/test/react-core-import-ban.test.ts`
|
|
@@ -27,7 +26,7 @@ import {
|
|
|
27
26
|
useSyncExternalStore,
|
|
28
27
|
} from 'react';
|
|
29
28
|
import type { z } from 'zod';
|
|
30
|
-
import type {
|
|
29
|
+
import type { RootStateObserver } from '../adapter';
|
|
31
30
|
import { getDebugRegistry } from '../runtime/debug-registry';
|
|
32
31
|
import { createFrameSelectorCache, type Equals, shallow } from '../runtime/frame-selector-cache';
|
|
33
32
|
import type { Game } from '../runtime/game';
|
|
@@ -39,7 +38,7 @@ export { shallow };
|
|
|
39
38
|
|
|
40
39
|
const GameContext = createContext<Game | null>(null);
|
|
41
40
|
|
|
42
|
-
/** Provide the `Game` to `
|
|
41
|
+
/** Provide the `Game` to `useWorldState`/`useGame` for everything mounted
|
|
43
42
|
* beneath it. Adapter-root hosts install this provider automatically — see the module doc
|
|
44
43
|
* comment above for the context-identity rule this hook family depends on.
|
|
45
44
|
*
|
|
@@ -48,38 +47,39 @@ const GameContext = createContext<Game | null>(null);
|
|
|
48
47
|
* the provider) rather than a bare `{ children: ReactNode }` field: a
|
|
49
48
|
* required `children` in `P` makes TS's `createElement<P>(type, props?:
|
|
50
49
|
* Attributes & P, ...children)` overload reject the plain 3-arg call
|
|
51
|
-
* `React.createElement(
|
|
50
|
+
* `React.createElement(WorldProvider, { game }, child)` (no JSX transform,
|
|
52
51
|
* e.g. a `.ts` main composing a sibling `.tsx` HUD) with "Property
|
|
53
52
|
* 'children' is missing" even though the 3rd arg supplies it — issue #97.
|
|
54
53
|
* Making `children` optional in the type lets that overload resolve; the
|
|
55
54
|
* rest-arg children are still wired through to `children` at runtime by
|
|
56
55
|
* React itself, unchanged. See `test/game-state-create-element-types.test.tsx`. */
|
|
57
|
-
export function
|
|
56
|
+
export function WorldProvider({ game, children }: PropsWithChildren<{ game: Game }>) {
|
|
58
57
|
return <GameContext.Provider value={game}>{children}</GameContext.Provider>;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
/** Read the `Game` provided by the nearest `
|
|
60
|
+
/** Read the `Game` provided by the nearest `WorldProvider`. Throws
|
|
62
61
|
* descriptively when called outside one — matching the repo's loud-failure
|
|
63
62
|
* habit rather than silently returning `null`. */
|
|
64
63
|
export function useGame(): Game {
|
|
65
64
|
const game = useContext(GameContext);
|
|
66
65
|
if (!game) {
|
|
67
66
|
throw new Error(
|
|
68
|
-
'useGame: no Game in context — wrap this component in <
|
|
67
|
+
'useGame: no Game in context — wrap this component in <WorldProvider game={ctx.game!}>.',
|
|
69
68
|
);
|
|
70
69
|
}
|
|
71
70
|
return game;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
|
-
* Subscribe to a selected slice of game state
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
74
|
+
* Subscribe to a selected slice of game state. Re-renders only when the
|
|
75
|
+
* selected value changes: the selector re-runs at most once per completed
|
|
76
|
+
* frame (`game.state.frameVersion`), and `equals` (default `Object.is`; pass
|
|
77
|
+
* {@link shallow} for tuples/objects) gates whether the fresh result
|
|
78
|
+
* actually counts as a change.
|
|
80
79
|
*
|
|
81
80
|
* `selector` reads the live graph (e.g.
|
|
82
|
-
* `g => g.
|
|
81
|
+
* `g => g.world('main')?.threeScene().getObjectByName('Hero')?.position.y`)
|
|
82
|
+
* at post-frame quiescence — no
|
|
83
83
|
* copies. A selector that returns a fresh object every call must pass
|
|
84
84
|
* `shallow` (or an equivalent `equals`), or it will re-render every frame.
|
|
85
85
|
*
|
|
@@ -88,7 +88,7 @@ export function useGame(): Game {
|
|
|
88
88
|
* `shallow`/`Object.is`, or a value that doesn't change across renders),
|
|
89
89
|
* not a fresh inline arrow every render.
|
|
90
90
|
*/
|
|
91
|
-
export function
|
|
91
|
+
export function useWorldState<T>(selector: (game: Game) => T, equals: Equals<T> = Object.is): T {
|
|
92
92
|
const game = useGame();
|
|
93
93
|
const cacheRef = useRef<ReturnType<typeof createFrameSelectorCache<T>> | null>(null);
|
|
94
94
|
if (cacheRef.current === null) {
|
|
@@ -102,27 +102,27 @@ export function useGameState<T>(selector: (game: Game) => T, equals: Equals<T> =
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Pure, react-free lookup for the `
|
|
105
|
+
* Pure, react-free lookup for the `useRootObservation` hook below — factored
|
|
106
106
|
* out so the hook body stays a thin `useSyncExternalStore` wrapper. Throws
|
|
107
107
|
* descriptively (loud, not `undefined`-forever) when `worldId` doesn't name a
|
|
108
108
|
* registered world, or that world's mount has no `observe`
|
|
109
|
-
* (`
|
|
109
|
+
* (`RootStateObserver`, `@engine/adapter`) — matching the `Game.registerRoot`
|
|
110
110
|
* "no state bridge" report (`runtime/game.ts`) this is the react-side half of.
|
|
111
111
|
*/
|
|
112
|
-
export function
|
|
112
|
+
export function requireRootObserver(game: Game, worldId: string): RootStateObserver {
|
|
113
113
|
const world = game.world(worldId);
|
|
114
114
|
if (!world) {
|
|
115
115
|
throw new Error(
|
|
116
|
-
`
|
|
116
|
+
`useRootObservation: no world registered with id "${worldId}" — check game.roots for the ` +
|
|
117
117
|
'ids actually registered.',
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
const observer = world.mounted.observe;
|
|
121
121
|
if (!observer) {
|
|
122
122
|
throw new Error(
|
|
123
|
-
`
|
|
124
|
-
'has no state bridge — its mount has no `observe` (
|
|
125
|
-
'be observed from React
|
|
123
|
+
`useRootObservation: world "${worldId}" (kind: ${world.kind}, adapter: "${world.adapter.id}") ` +
|
|
124
|
+
'has no state bridge — its mount has no `observe` (RootStateObserver). This world cannot ' +
|
|
125
|
+
'be observed from React.',
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
return observer;
|
|
@@ -130,32 +130,31 @@ export function requireWorldObserver(game: Game, worldId: string): WorldStateObs
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Subscribe to a selected slice of a FOREIGN (non-first-party) world's
|
|
133
|
-
* observed state
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* through one mental model.
|
|
133
|
+
* observed state — the ingested-world counterpart of `useWorldState` above;
|
|
134
|
+
* both are sugar over the same subscribe/snapshot shape, so a react HUD reads
|
|
135
|
+
* native and ingested sources through one mental model.
|
|
137
136
|
*
|
|
138
137
|
* `selector` runs over that world's `snapshot()` — NOT the `Game` — since a
|
|
139
|
-
* foreign world
|
|
138
|
+
* foreign world exposes no first-party state. Re-renders only when the
|
|
140
139
|
* selected value changes (`equals`, default `Object.is`; pass {@link shallow}
|
|
141
140
|
* for tuples/objects returned fresh every call).
|
|
142
141
|
*
|
|
143
|
-
* Cadence note: unlike `
|
|
142
|
+
* Cadence note: unlike `useWorldState`, this does NOT key its cache off
|
|
144
143
|
* `game.state.frameVersion` — a foreign/self-driven world may notify on its
|
|
145
144
|
* OWN rAF cadence, unrelated to our frame timing (§4's "consumers must not
|
|
146
145
|
* assume our frame timing"). Instead this keeps a local version counter,
|
|
147
146
|
* bumped once per `onChange` notification from the observer itself.
|
|
148
147
|
*
|
|
149
148
|
* Throws (loud, not a silently-undefined subscription) if `worldId` isn't
|
|
150
|
-
* registered, or its mount has no `observe` — see `
|
|
149
|
+
* registered, or its mount has no `observe` — see `requireRootObserver`.
|
|
151
150
|
*/
|
|
152
|
-
export function
|
|
151
|
+
export function useRootObservation<T>(
|
|
153
152
|
worldId: string,
|
|
154
153
|
selector: (snapshot: unknown) => T,
|
|
155
154
|
equals: Equals<T> = Object.is,
|
|
156
155
|
): T {
|
|
157
156
|
const game = useGame();
|
|
158
|
-
const observer =
|
|
157
|
+
const observer = requireRootObserver(game, worldId);
|
|
159
158
|
const cacheRef = useRef<ReturnType<typeof createFrameSelectorCache<T>> | null>(null);
|
|
160
159
|
if (cacheRef.current === null) {
|
|
161
160
|
cacheRef.current = createFrameSelectorCache<T>(equals);
|
|
@@ -176,19 +175,18 @@ export function useWorldObservation<T>(
|
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
/**
|
|
179
|
-
* The debug seam's react-world registration door (Task 1.5
|
|
180
|
-
* `
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* through `game.systemAdapters.debug.state(name)` exactly like a
|
|
178
|
+
* The debug seam's react-world registration door (Task 1.5): react roots
|
|
179
|
+
* have no `setup()`/ `ctx.debug` of their own, so this hook registers a
|
|
180
|
+
* named state provider into the SAME game-scoped registry every root's
|
|
181
|
+
* `ctx.debug` feeds (`getDebugRegistry(game)`, `runtime/debug-registry.ts`),
|
|
182
|
+
* readable through `game.systemAdapters.debug.state(name)` exactly like a
|
|
185
183
|
* `ctx.debug.registerStateProvider` call.
|
|
186
184
|
*
|
|
187
185
|
* Registers ONCE per mount (keyed on `name`) and unregisters on unmount —
|
|
188
186
|
* silently, like hot-reload's `strip()`. Re-renders never re-register or
|
|
189
187
|
* re-warn: `fn` is kept in a ref updated every render, so the registered
|
|
190
188
|
* closure always calls the CURRENT `fn` without touching the registry.
|
|
191
|
-
* Outside a `<
|
|
189
|
+
* Outside a `<WorldProvider>` (or against a bare test `Game` built without a
|
|
192
190
|
* registry) this is an inert no-op — it never throws, matching the "no seam
|
|
193
191
|
* without a consumer" degrade every optional `ctx.*` surface follows.
|
|
194
192
|
*/
|
|
@@ -219,14 +217,14 @@ export function useDebugProvider(
|
|
|
219
217
|
* Same mount/unmount/ref-latest contract as {@link useDebugProvider}: `spec`
|
|
220
218
|
* (description/args/locus) is captured at registration time, `fn` always
|
|
221
219
|
* calls through to the latest render's closure via a ref, and the hook is an
|
|
222
|
-
* inert no-op with no `<
|
|
220
|
+
* inert no-op with no `<WorldProvider>` in scope.
|
|
223
221
|
*
|
|
224
222
|
* Generic over the declared `args` Zod tuple, same as
|
|
225
223
|
* `DebugCtxSurface.registerCommand` ({@link DebugCommandArgs}, dry-run
|
|
226
|
-
* finding —
|
|
227
|
-
* `
|
|
228
|
-
* `
|
|
229
|
-
*
|
|
224
|
+
* finding — ledger): declaring `args: z.tuple([z.number(), z.string()])`
|
|
225
|
+
* types `fn`'s parameters as `(n: number, s: string) => ...` with no
|
|
226
|
+
* `unknown[]` cast; omitting `args` keeps `fn` typed `(...args: unknown[])
|
|
227
|
+
* => ...` as before.
|
|
230
228
|
*/
|
|
231
229
|
export function useDebugCommand<T extends z.ZodTuple | undefined = undefined>(
|
|
232
230
|
name: string,
|
|
@@ -259,7 +257,7 @@ export function useDebugCommand<T extends z.ZodTuple | undefined = undefined>(
|
|
|
259
257
|
* emit('score-changed', { score });
|
|
260
258
|
* ```
|
|
261
259
|
*
|
|
262
|
-
* It is an inert no-op outside a `<
|
|
260
|
+
* It is an inert no-op outside a `<WorldProvider>`, matching the optional
|
|
263
261
|
* behavior of the other React debug hooks. Games never need to import the
|
|
264
262
|
* debug registry or know its internal React provenance id.
|
|
265
263
|
*/
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Transparent auto-batcher (Strategy 1
|
|
5
|
-
* and FAST-RENDER-BY-DEFAULT.md).
|
|
4
|
+
* Transparent auto-batcher (Strategy 1).
|
|
6
5
|
*
|
|
7
6
|
* The framework keeps each entity's Object3D as the logical handle (transform,
|
|
8
7
|
* gameplay, physics, picking) and renders eligible entities through a shared
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import type { InstancesFile } from '
|
|
2
|
+
import type { InstancesFile } from '../asset-formats/instances';
|
|
3
3
|
|
|
4
4
|
/** Build one native InstancedMesh from a parsed `.instances.json` tuple array. */
|
|
5
5
|
export function buildInstancedMeshFromTuples(
|
|
@@ -9,31 +9,32 @@
|
|
|
9
9
|
* edit to one copy (or to `DEFAULTS`) could silently diverge from the other.
|
|
10
10
|
*
|
|
11
11
|
* This module is now the ONE place that builds a `THREE.Light`/`THREE.Camera`
|
|
12
|
-
* from a `
|
|
12
|
+
* from a `LightDescriptor`/`CameraDescriptor` descriptor, reading its fallbacks from
|
|
13
13
|
* `DEFAULTS` (the single source of truth for scene default values — see
|
|
14
|
-
* `
|
|
14
|
+
* `defaults.ts`). Both the loader and the editor import it, so they
|
|
15
15
|
* can no longer disagree.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import * as THREE from 'three';
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
19
|
+
// Area lights. `RectAreaLight` requires its LTC lookup-texture uniforms
|
|
20
|
+
// initialized once before the first instance is constructed; three.js ships
|
|
21
|
+
// that setup as an addon, not part of core. Imported via the
|
|
22
|
+
// `three/addons/*` alias (package.json `exports`: `"./addons/*":
|
|
23
|
+
// "./examples/jsm/*"` — the identical file as
|
|
24
24
|
// `three/examples/jsm/lights/RectAreaLightUniformsLib.js`) because a literal
|
|
25
25
|
// `/examples/` specifier trips `engine-host-no-game-imports.test.ts`'s
|
|
26
26
|
// game/editor-example guard (a same-substring false positive against this
|
|
27
27
|
// repo's OWN `packages/editor/template/src/examples/` convention, unrelated
|
|
28
28
|
// to three.js's addon folder naming).
|
|
29
29
|
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
|
|
30
|
-
import {
|
|
31
|
-
import type {
|
|
30
|
+
import type { CameraDescriptor } from '../asset-formats/camera';
|
|
31
|
+
import type { LightDescriptor } from '../asset-formats/light';
|
|
32
|
+
import { DEFAULTS } from '../defaults';
|
|
32
33
|
|
|
33
34
|
let rectAreaLightUniformsInitialized = false;
|
|
34
35
|
|
|
35
|
-
/** Build a Three.js light from a `
|
|
36
|
-
export function createLight(def:
|
|
36
|
+
/** Build a Three.js light from a `LightDescriptor` descriptor. */
|
|
37
|
+
export function createLight(def: LightDescriptor): THREE.Light {
|
|
37
38
|
const d = DEFAULTS.light;
|
|
38
39
|
const color = def.color ?? d.color;
|
|
39
40
|
const intensity = def.intensity ?? d.intensity;
|
|
@@ -81,8 +82,8 @@ export function createLight(def: SceneLight): THREE.Light {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
/** Build a Three.js camera from a `
|
|
85
|
-
export function createCamera(def:
|
|
85
|
+
/** Build a Three.js camera from a `CameraDescriptor` descriptor. */
|
|
86
|
+
export function createCamera(def: CameraDescriptor): THREE.Camera {
|
|
86
87
|
const d = DEFAULTS.camera;
|
|
87
88
|
const aspect = (def.width ?? d.width) / (def.height ?? d.height);
|
|
88
89
|
if (def.type === 'orthographic') {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-frame LOD tick. `THREE.LOD` only swaps its active level when
|
|
5
|
+
* `.update(camera)` runs, so every `THREE.LOD` in the scene graph needs this
|
|
6
|
+
* called once a frame. Wired into the runtime's `preRender` system in
|
|
7
|
+
* `adapter/setup-three-root-adapter.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Format-neutral: it matches THREE's own `isLOD` marker and knows nothing about
|
|
10
|
+
* how the LOD was built. Formerly `@engine/scene/scene-loader`'s
|
|
11
|
+
* `updateSceneLODs`, relocated here when WO-8 deleted the `.vscn` scene loader.
|
|
12
|
+
*/
|
|
13
|
+
export function updateSceneLODs(scene: THREE.Object3D, camera: THREE.Camera): void {
|
|
14
|
+
scene.traverse((o) => {
|
|
15
|
+
if ((o as THREE.LOD).isLOD) (o as THREE.LOD).update(camera);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import type { MaterialDescriptor } from '../asset-formats/material';
|
|
3
|
+
import type { MeshDescriptor } from '../asset-formats/mesh';
|
|
4
|
+
import { loadTexture } from '../asset-loaders';
|
|
5
|
+
import { DEFAULTS } from '../defaults';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Single source of truth for turning a `
|
|
8
|
+
* Single source of truth for turning a `MeshDescriptor` / `MaterialDescriptor` descriptor
|
|
8
9
|
* into Three.js geometry/material. Both the runtime scene-loader and the editor's
|
|
9
10
|
* entity-factory import these, so the editor preview renders exactly what the game
|
|
10
|
-
* renders (the WYSIWYG guarantee
|
|
11
|
+
* renders (the WYSIWYG guarantee). Previously these were
|
|
11
12
|
* copy-pasted in two places and had to be hand-synced — any new material feature
|
|
12
13
|
* added to one and not the other silently broke that guarantee.
|
|
13
14
|
*
|
|
14
15
|
*/
|
|
15
16
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward type-based switch with fallback args
|
|
16
|
-
export function createGeometry(mesh:
|
|
17
|
+
export function createGeometry(mesh: MeshDescriptor): THREE.BufferGeometry {
|
|
17
18
|
const args = mesh.args ?? [];
|
|
18
19
|
switch (mesh.type) {
|
|
19
20
|
case 'box':
|
|
@@ -40,7 +41,7 @@ export function createGeometry(mesh: SceneMesh): THREE.BufferGeometry {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: flat type-based switch building Three materials
|
|
43
|
-
export function createMaterial(mat?:
|
|
44
|
+
export function createMaterial(mat?: MaterialDescriptor): THREE.Material {
|
|
44
45
|
if (!mat) return new THREE.MeshStandardMaterial({ color: DEFAULTS.material.color });
|
|
45
46
|
|
|
46
47
|
// Only include texture props when actually defined (Three.js warns on undefined values)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all lint/suspicious/noExplicitAny: quarks' API uses untyped FunctionJSON internally; our Zod types are structurally compatible but TS can't unify them
|
|
2
2
|
/**
|
|
3
|
-
* Converts
|
|
3
|
+
* Converts ParticlesDescriptor JSON → three.quarks ParticleSystem.
|
|
4
4
|
*
|
|
5
5
|
* Shared between the editor entity-factory and the runtime scene-loader.
|
|
6
6
|
* The schema mirrors quarks' native JSON format, so most fields pass through
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ColorOverLife,
|
|
16
16
|
ConeEmitter,
|
|
17
17
|
DonutEmitter,
|
|
18
|
+
type EmitterShape,
|
|
18
19
|
ForceOverLife,
|
|
19
20
|
FrameOverLife,
|
|
20
21
|
GravityForce,
|
|
@@ -36,13 +37,13 @@ import {
|
|
|
36
37
|
} from 'quarks.core';
|
|
37
38
|
import * as THREE from 'three';
|
|
38
39
|
import { type BatchedRenderer, ParticleSystem, RenderMode } from 'three.quarks';
|
|
39
|
-
import type { SceneParticles } from './scene-types';
|
|
40
40
|
import type {
|
|
41
41
|
BehaviorJSON,
|
|
42
42
|
ColorGeneratorJSON,
|
|
43
43
|
EmitterShapeJSON,
|
|
44
|
+
ParticlesDescriptor,
|
|
44
45
|
ValueGeneratorJSON,
|
|
45
|
-
} from '
|
|
46
|
+
} from '../asset-formats/particles';
|
|
46
47
|
|
|
47
48
|
// --- Render mode mapping ---
|
|
48
49
|
|
|
@@ -203,7 +204,7 @@ function createBehavior(json: BehaviorJSON): any {
|
|
|
203
204
|
|
|
204
205
|
// --- Material ---
|
|
205
206
|
|
|
206
|
-
function createParticleMaterial(mat:
|
|
207
|
+
function createParticleMaterial(mat: ParticlesDescriptor['material']): THREE.Material {
|
|
207
208
|
const textures: Record<string, THREE.Texture> = {};
|
|
208
209
|
if (mat.map) textures['map'] = loadParticleTexture(mat.map);
|
|
209
210
|
|
|
@@ -267,14 +268,59 @@ export interface ParticleSystemResult {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
/**
|
|
270
|
-
*
|
|
271
|
+
* The non-JSON companion to a {@link ParticlesDescriptor} — the runtime objects
|
|
272
|
+
* a JSON document cannot carry, and an authoring toggle. Every field is
|
|
273
|
+
* optional and the whole argument is optional, so existing single-argument
|
|
274
|
+
* callers (the editor entity-factory, the runtime scene-loader) are unchanged.
|
|
275
|
+
*
|
|
276
|
+
* This is how a caller with fidelity a plain descriptor cannot express reaches
|
|
277
|
+
* the SAME factory instead of hand-building its own `new ParticleSystem(...)`:
|
|
278
|
+
* a foreign-engine carry (e.g. Godot CPUParticles) supplies its own
|
|
279
|
+
* `EmitterShape` and an instancing geometry here, and expresses everything
|
|
280
|
+
* else — lifecycle, start values, emission, behaviors, material — as descriptor
|
|
281
|
+
* JSON.
|
|
282
|
+
*/
|
|
283
|
+
export interface ParticleSystemObjects {
|
|
284
|
+
/**
|
|
285
|
+
* A pre-built emitter shape for a spawn distribution the JSON `shape` field
|
|
286
|
+
* cannot express (a `three.quarks` `EmitterShape` plugin instance). When
|
|
287
|
+
* present it REPLACES `data.shape`.
|
|
288
|
+
*/
|
|
289
|
+
shape?: EmitterShape;
|
|
290
|
+
/**
|
|
291
|
+
* The geometry instanced per particle in `RenderMode.Mesh`. A
|
|
292
|
+
* `BufferGeometry` is not JSON-serializable, so `renderMode: 'mesh'` requires
|
|
293
|
+
* the caller to resolve and pass it here.
|
|
294
|
+
*/
|
|
295
|
+
instancingGeometry?: THREE.BufferGeometry;
|
|
296
|
+
/**
|
|
297
|
+
* When the material has no texture `map`, the factory substitutes a soft
|
|
298
|
+
* radial default sprite so a freshly-authored billboard shows something.
|
|
299
|
+
* Pass `false` to keep the material's own (map-less) appearance — a faithful
|
|
300
|
+
* carry of a foreign engine whose particle draws a plain colored quad, and
|
|
301
|
+
* the way that carry avoids the default-sprite canvas entirely. Defaults to
|
|
302
|
+
* `true`.
|
|
303
|
+
*/
|
|
304
|
+
defaultSprite?: boolean;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create a three.quarks ParticleSystem from ParticlesDescriptor JSON data.
|
|
309
|
+
*
|
|
310
|
+
* `objects` carries the pieces a JSON descriptor cannot: a plugin emitter
|
|
311
|
+
* shape, a mesh-mode instancing geometry, and the default-sprite toggle. It is
|
|
312
|
+
* optional and additive — a call with only `data` behaves exactly as before.
|
|
271
313
|
*/
|
|
272
314
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward field-by-field mapping
|
|
273
|
-
export function createParticleSystemFromData(
|
|
315
|
+
export function createParticleSystemFromData(
|
|
316
|
+
data: ParticlesDescriptor,
|
|
317
|
+
objects?: ParticleSystemObjects,
|
|
318
|
+
): ParticleSystemResult {
|
|
274
319
|
const material = createParticleMaterial(data.material);
|
|
275
320
|
|
|
276
|
-
// Apply default sprite if no texture specified
|
|
277
|
-
|
|
321
|
+
// Apply default sprite if no texture specified (unless the caller opts out to
|
|
322
|
+
// keep a map-less material faithful to its source).
|
|
323
|
+
if ((objects?.defaultSprite ?? true) && !data.material.map && 'map' in material) {
|
|
278
324
|
(material as THREE.MeshBasicMaterial).map = getDefaultSpriteTex();
|
|
279
325
|
}
|
|
280
326
|
|
|
@@ -285,7 +331,11 @@ export function createParticleSystemFromData(data: SceneParticles): ParticleSyst
|
|
|
285
331
|
if (data.looping != null) params['looping'] = data.looping;
|
|
286
332
|
if (data.prewarm != null) params['prewarm'] = data.prewarm;
|
|
287
333
|
if (data.duration != null) params['duration'] = data.duration;
|
|
288
|
-
|
|
334
|
+
// A pre-built shape (a distribution the JSON cannot express) wins over the
|
|
335
|
+
// descriptor's own shape; otherwise build the shape from JSON.
|
|
336
|
+
if (objects?.shape) params['shape'] = objects.shape;
|
|
337
|
+
else if (data.shape) params['shape'] = createEmitterShape(data.shape);
|
|
338
|
+
if (objects?.instancingGeometry) params['instancingGeometry'] = objects.instancingGeometry;
|
|
289
339
|
|
|
290
340
|
if (data.startLife) params['startLife'] = valueGen(data.startLife);
|
|
291
341
|
if (data.startSpeed) params['startSpeed'] = valueGen(data.startSpeed);
|
|
@@ -329,7 +379,7 @@ export function createParticleSystemFromData(data: SceneParticles): ParticleSyst
|
|
|
329
379
|
}
|
|
330
380
|
|
|
331
381
|
/**
|
|
332
|
-
* Patch a RUNNING ParticleSystem in place from
|
|
382
|
+
* Patch a RUNNING ParticleSystem in place from ParticlesDescriptor JSON — the
|
|
333
383
|
* editor's live-tune path (W1b). Unlike `createParticleSystemFromData`, this
|
|
334
384
|
* does NOT recreate the system: the particle pool, per-particle ages, and the
|
|
335
385
|
* emission clock all survive, so a curve-key drag reshapes the live emitter
|
|
@@ -345,7 +395,7 @@ export function createParticleSystemFromData(data: SceneParticles): ParticleSyst
|
|
|
345
395
|
* teardown/rebuild path): material, renderMode, tiling/soft-particle render
|
|
346
396
|
* settings, worldSpace.
|
|
347
397
|
*/
|
|
348
|
-
export function applyParticlesDataLive(system: ParticleSystem, data:
|
|
398
|
+
export function applyParticlesDataLive(system: ParticleSystem, data: ParticlesDescriptor): void {
|
|
349
399
|
if (data.duration != null) system.duration = data.duration;
|
|
350
400
|
if (data.looping != null) system.looping = data.looping;
|
|
351
401
|
if (data.prewarm != null) system.prewarm = data.prewarm;
|
|
@@ -410,7 +460,7 @@ export function unregisterParticleSystem(
|
|
|
410
460
|
/**
|
|
411
461
|
* Default particle data for creating a new particle emitter via AddSectionMenu.
|
|
412
462
|
*/
|
|
413
|
-
export function defaultParticlesData():
|
|
463
|
+
export function defaultParticlesData(): ParticlesDescriptor {
|
|
414
464
|
return {
|
|
415
465
|
looping: true,
|
|
416
466
|
duration: 2,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import { getUserData } from '../
|
|
2
|
+
import { getUserData } from '../ecs/user-data';
|
|
3
3
|
import type { ResolvedRenderSettings } from './render-settings';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -12,9 +12,8 @@ import type { ResolvedRenderSettings } from './render-settings';
|
|
|
12
12
|
* N logical objects → 1 draw call, ~0 CPU/frame. This is the classic "static
|
|
13
13
|
* batching" technique (cf. Unity/PlayCanvas static batch groups).
|
|
14
14
|
*
|
|
15
|
-
* Strategy 1 (keep identity, don't merge)
|
|
16
|
-
*
|
|
17
|
-
* all-frame churn) are a separate concern — see render/auto-batcher.ts (dynamic).
|
|
15
|
+
* Strategy 1 (keep identity, don't merge). Moving entities (boids-style, all-frame churn) are a separate
|
|
16
|
+
* concern — see render/auto-batcher.ts (dynamic).
|
|
18
17
|
*
|
|
19
18
|
* Eligibility (anything failing stays an ordinary draw — degrade, don't break):
|
|
20
19
|
* - plain THREE.Mesh (not Instanced/Skinned/Batched)
|
|
@@ -28,8 +27,6 @@ import type { ResolvedRenderSettings } from './render-settings';
|
|
|
28
27
|
* - Runs once per scene load (build() is called after loadScene). Entities spawned
|
|
29
28
|
* AFTER build do not join a batch; call build() again to re-scan. Removing a batched
|
|
30
29
|
* source leaves a stale instance until rebuild. (Static scenery is load-time stable.)
|
|
31
|
-
* - Installed for scene-file loads (scenePath/sceneData), not the imperative setup()
|
|
32
|
-
* path — setup() games manage their own scene/spawning.
|
|
33
30
|
* - The batch culls as ONE unit (no per-instance frustum culling yet — Tier-2).
|
|
34
31
|
*/
|
|
35
32
|
|
|
@@ -61,29 +58,6 @@ const SIG = (m: THREE.Mesh): string =>
|
|
|
61
58
|
// (the InstancedMesh carries one flag for the whole group).
|
|
62
59
|
`${geoKey(m.geometry as THREE.BufferGeometry)}|${matKey(m.material as THREE.Material)}|${m.castShadow ? 1 : 0}${m.receiveShadow ? 1 : 0}`;
|
|
63
60
|
|
|
64
|
-
interface EntityDef {
|
|
65
|
-
render?: { autoBatch?: boolean };
|
|
66
|
-
physics?: unknown;
|
|
67
|
-
components?: Record<string, unknown>;
|
|
68
|
-
animation?: unknown;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Nearest self/ancestor entity descriptor, if any. */
|
|
72
|
-
function nearestEntity(obj: THREE.Object3D): EntityDef | undefined {
|
|
73
|
-
let cur: THREE.Object3D | null = obj;
|
|
74
|
-
while (cur) {
|
|
75
|
-
const entity = getUserData(cur, 'entity') as unknown as EntityDef | undefined;
|
|
76
|
-
if (entity) return entity;
|
|
77
|
-
cur = cur.parent;
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Explicit per-entity opt-out (`render.autoBatch === false`). */
|
|
83
|
-
function entityOptOut(obj: THREE.Object3D): boolean {
|
|
84
|
-
return nearestEntity(obj)?.render?.autoBatch === false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
61
|
/** Match Three.js renderer visibility: a hidden ancestor hides the whole subtree. */
|
|
88
62
|
function isEffectivelyVisible(obj: THREE.Object3D): boolean {
|
|
89
63
|
let cur: THREE.Object3D | null = obj;
|
|
@@ -103,23 +77,22 @@ function isBatchCandidate(object: THREE.Object3D, batchRoot: THREE.Group): objec
|
|
|
103
77
|
if (!isEffectivelyVisible(mesh)) return false;
|
|
104
78
|
if (!mesh.geometry || !mesh.material || Array.isArray(mesh.material)) return false;
|
|
105
79
|
if ((mesh.material as THREE.Material).transparent) return false;
|
|
106
|
-
|
|
107
|
-
return !entityOptOut(mesh);
|
|
80
|
+
return batchRoot !== mesh.parent;
|
|
108
81
|
}
|
|
109
82
|
|
|
110
83
|
/**
|
|
111
|
-
* Static-batching eligibility: only
|
|
112
|
-
* and instance. An
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
84
|
+
* Static-batching eligibility: only objects that won't move are safe to freeze
|
|
85
|
+
* and instance. An object (or an ancestor) carrying its own animation mixer is
|
|
86
|
+
* a likely mover → left as an ordinary draw. Plain scenery is eligible. This is
|
|
87
|
+
* the safe, transparent default; a future dynamic-batch path (markMoved-driven)
|
|
88
|
+
* can include movers.
|
|
116
89
|
*/
|
|
117
90
|
function isLikelyStatic(obj: THREE.Object3D): boolean {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
91
|
+
let cur: THREE.Object3D | null = obj;
|
|
92
|
+
while (cur) {
|
|
93
|
+
if (getUserData(cur, '_animMixer')) return false;
|
|
94
|
+
cur = cur.parent;
|
|
95
|
+
}
|
|
123
96
|
return true;
|
|
124
97
|
}
|
|
125
98
|
|
|
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
* overlay are all derived from this list, so "everything togglable" stays
|
|
8
8
|
* consistent by construction (same posture as the component/material/instancer
|
|
9
9
|
* registries). Adding a feature here gives it a schema field + cascade entry for
|
|
10
|
-
* free
|
|
10
|
+
* free..
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export type RenderFeatureValue = boolean | number | string;
|
|
@@ -2,8 +2,7 @@ import { RENDER_FEATURES, type RenderFeatureValue } from './render-features';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Cascade resolution for render settings: engine default → scene → entity, with
|
|
5
|
-
* tri-state inherit (an absent value inherits from the next level up)
|
|
6
|
-
* docs/research/RENDER-SETTINGS-UX.md.
|
|
5
|
+
* tri-state inherit (an absent value inherits from the next level up)..
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
/** A partial override map (scene-level or entity-level). Absent key = inherit. */
|