lecodes-sdk 2.4.4 → 2.4.5

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/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "marcidb-embedded": "^0.15.0",
44
44
  "sharp": "^0.35.2"
45
45
  },
46
- "version": "2.4.4",
46
+ "version": "2.4.5",
47
47
  "files": [
48
48
  "src",
49
49
  "dist",
@@ -1,175 +1,175 @@
1
- // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
- // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
- // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
- // builder every target class delegates to (`tweenBag`).
5
-
6
- import { TweenAnimation, type Animation } from "./Animation"
7
- import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
- import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
-
10
- /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
- const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
-
13
- const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
- clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
-
16
- /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
- export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
- const meta = bag as TweenMeta
19
- const tracks: Track[] = []
20
- for (const [prop, value] of bagProps(bag)) {
21
- const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
- if (track) tracks.push(track)
23
- }
24
- return tracks
25
- }
26
-
27
- /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
- export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
- const meta = bag as TweenMeta
30
- const tracks = bagTracks(target, bag, 0, fromCurrent)
31
- let durationMs = 0
32
- for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
- const spec: TweenSpec = {
34
- clock: clockOf(meta.clock, target._tweenClock),
35
- durationMs, delayMs: 0,
36
- iterations: iterationsOf(meta.loop),
37
- pingPong: meta.loopMode !== "restart",
38
- rate: 1, tracks, calls: [],
39
- }
40
- return new TweenAnimation(spec).play()
41
- }
42
-
43
- export type TimelineOptions = {
44
- /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
- clock?: "ui" | "game",
46
- /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
- loop?: boolean | number,
48
- loopMode?: "restart" | "ping-pong",
49
- /** Wait before the first play, ms. */
50
- delay?: number,
51
- }
52
-
53
- /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
- export type TimelinePosition = number | string | [string, number]
55
-
56
- export type TimelineAddOptions = {
57
- at?: TimelinePosition,
58
- /** With several targets: each starts this many ms after the previous one. */
59
- stagger?: number,
60
- }
61
-
62
- /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
- * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
- * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
- * starts, the last one after it ends). */
66
- export class TimelineImpl extends TweenAnimation {
67
- private _labels = new Map<string, number>()
68
- private _end = 0
69
-
70
- constructor(options: TimelineOptions = {}) {
71
- super({
72
- clock: clockOf(options.clock, CLOCK_UI),
73
- durationMs: 0,
74
- delayMs: options.delay ?? 0,
75
- iterations: iterationsOf(options.loop),
76
- pingPong: options.loopMode !== "restart",
77
- rate: 1, tracks: [], calls: [],
78
- })
79
- }
80
-
81
- private _at(at: TimelinePosition | undefined): number {
82
- if (at === undefined) return this._end
83
- if (typeof at === "number") return at
84
- const name = Array.isArray(at) ? at[0] : at
85
- const offset = Array.isArray(at) ? at[1] : 0
86
- const base = this._labels.get(name)
87
- if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
- return base + offset
89
- }
90
-
91
- private _grow(endMs: number): void {
92
- if (endMs > this._end) this._end = endMs
93
- this._spec.durationMs = this._end
94
- }
95
-
96
- /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
- * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
- add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
- const at = this._at(options.at)
100
- const targets = Array.isArray(target) ? target : [target]
101
- const stagger = options.stagger ?? 0
102
- targets.forEach((t, i) => {
103
- const tracks = bagTracks(t, props, at + i * stagger, true)
104
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
- })
106
- return this
107
- }
108
-
109
- /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
- addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
- const at = this._at(options.at)
112
- const targets = Array.isArray(target) ? target : [target]
113
- const stagger = options.stagger ?? 0
114
- targets.forEach((t, i) => {
115
- const tracks = bagTracks(t, props, at + i * stagger, false)
116
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
- })
118
- return this
119
- }
120
-
121
- /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
- * every frame with the value. */
123
- animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
- const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
- this._values.set(slot, deliver)
126
- this._spec.tracks.push(track)
127
- this._grow(track.atMs + track.durMs)
128
- return this
129
- }
130
-
131
- /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
- * finish, never from a previous run. */
133
- call(at: TimelinePosition, fn: () => void): this {
134
- const t = this._at(at)
135
- this._spec.calls.push(t)
136
- this._calls.push(fn)
137
- this._grow(t)
138
- return this
139
- }
140
-
141
- /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
- label(name: string, at?: TimelinePosition): this {
143
- this._labels.set(name, this._at(at))
144
- return this
145
- }
146
-
147
- /** Named times, ms. */
148
- get labels(): Record<string, number> {
149
- const o: Record<string, number> = {}
150
- for (const [k, v] of this._labels) o[k] = v
151
- return o
152
- }
153
-
154
- /** Extend the timeline with stillness after its current end. */
155
- hold(ms: number): this {
156
- this._grow(this._end + Math.max(0, ms))
157
- return this
158
- }
159
-
160
- /** The current end, ms (tracks, calls and holds). */
161
- get end(): number { return this._end }
162
-
163
- /** `seek` also accepts a label. */
164
- override seek(at: number | string): this {
165
- return super.seek(typeof at === "string" ? this._at(at) : at)
166
- }
167
- }
168
-
169
- /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
- export type Timeline = TimelineImpl
171
-
172
- /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
- export function Timeline(options?: TimelineOptions): Timeline {
174
- return new TimelineImpl(options)
175
- }
1
+ // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
+ // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
+ // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
+ // builder every target class delegates to (`tweenBag`).
5
+
6
+ import { TweenAnimation, type Animation } from "./Animation"
7
+ import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
+ import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
+
10
+ /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
+ const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
+
13
+ const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
+ clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
+
16
+ /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
+ export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
+ const meta = bag as TweenMeta
19
+ const tracks: Track[] = []
20
+ for (const [prop, value] of bagProps(bag)) {
21
+ const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
+ if (track) tracks.push(track)
23
+ }
24
+ return tracks
25
+ }
26
+
27
+ /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
+ export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
+ const meta = bag as TweenMeta
30
+ const tracks = bagTracks(target, bag, 0, fromCurrent)
31
+ let durationMs = 0
32
+ for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
+ const spec: TweenSpec = {
34
+ clock: clockOf(meta.clock, target._tweenClock),
35
+ durationMs, delayMs: 0,
36
+ iterations: iterationsOf(meta.loop),
37
+ pingPong: meta.loopMode !== "restart",
38
+ rate: 1, tracks, calls: [],
39
+ }
40
+ return new TweenAnimation(spec).play()
41
+ }
42
+
43
+ export type TimelineOptions = {
44
+ /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
+ clock?: "ui" | "game",
46
+ /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
+ loop?: boolean | number,
48
+ loopMode?: "restart" | "ping-pong",
49
+ /** Wait before the first play, ms. */
50
+ delay?: number,
51
+ }
52
+
53
+ /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
+ export type TimelinePosition = number | string | [string, number]
55
+
56
+ export type TimelineAddOptions = {
57
+ at?: TimelinePosition,
58
+ /** With several targets: each starts this many ms after the previous one. */
59
+ stagger?: number,
60
+ }
61
+
62
+ /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
+ * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
+ * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
+ * starts, the last one after it ends). */
66
+ export class TimelineImpl extends TweenAnimation {
67
+ private _labels = new Map<string, number>()
68
+ private _end = 0
69
+
70
+ constructor(options: TimelineOptions = {}) {
71
+ super({
72
+ clock: clockOf(options.clock, CLOCK_UI),
73
+ durationMs: 0,
74
+ delayMs: options.delay ?? 0,
75
+ iterations: iterationsOf(options.loop),
76
+ pingPong: options.loopMode !== "restart",
77
+ rate: 1, tracks: [], calls: [],
78
+ })
79
+ }
80
+
81
+ private _at(at: TimelinePosition | undefined): number {
82
+ if (at === undefined) return this._end
83
+ if (typeof at === "number") return at
84
+ const name = Array.isArray(at) ? at[0] : at
85
+ const offset = Array.isArray(at) ? at[1] : 0
86
+ const base = this._labels.get(name)
87
+ if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
+ return base + offset
89
+ }
90
+
91
+ private _grow(endMs: number): void {
92
+ if (endMs > this._end) this._end = endMs
93
+ this._spec.durationMs = this._end
94
+ }
95
+
96
+ /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
+ * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
+ add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
+ const at = this._at(options.at)
100
+ const targets = Array.isArray(target) ? target : [target]
101
+ const stagger = options.stagger ?? 0
102
+ targets.forEach((t, i) => {
103
+ const tracks = bagTracks(t, props, at + i * stagger, true)
104
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
+ })
106
+ return this
107
+ }
108
+
109
+ /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
+ addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
+ const at = this._at(options.at)
112
+ const targets = Array.isArray(target) ? target : [target]
113
+ const stagger = options.stagger ?? 0
114
+ targets.forEach((t, i) => {
115
+ const tracks = bagTracks(t, props, at + i * stagger, false)
116
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
+ })
118
+ return this
119
+ }
120
+
121
+ /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
+ * every frame with the value. */
123
+ animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
+ const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
+ this._values.set(slot, deliver)
126
+ this._spec.tracks.push(track)
127
+ this._grow(track.atMs + track.durMs)
128
+ return this
129
+ }
130
+
131
+ /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
+ * finish, never from a previous run. */
133
+ call(at: TimelinePosition, fn: () => void): this {
134
+ const t = this._at(at)
135
+ this._spec.calls.push(t)
136
+ this._calls.push(fn)
137
+ this._grow(t)
138
+ return this
139
+ }
140
+
141
+ /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
+ label(name: string, at?: TimelinePosition): this {
143
+ this._labels.set(name, this._at(at))
144
+ return this
145
+ }
146
+
147
+ /** Named times, ms. */
148
+ get labels(): Record<string, number> {
149
+ const o: Record<string, number> = {}
150
+ for (const [k, v] of this._labels) o[k] = v
151
+ return o
152
+ }
153
+
154
+ /** Extend the timeline with stillness after its current end. */
155
+ hold(ms: number): this {
156
+ this._grow(this._end + Math.max(0, ms))
157
+ return this
158
+ }
159
+
160
+ /** The current end, ms (tracks, calls and holds). */
161
+ get end(): number { return this._end }
162
+
163
+ /** `seek` also accepts a label. */
164
+ override seek(at: number | string): this {
165
+ return super.seek(typeof at === "string" ? this._at(at) : at)
166
+ }
167
+ }
168
+
169
+ /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
+ export type Timeline = TimelineImpl
171
+
172
+ /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
+ export function Timeline(options?: TimelineOptions): Timeline {
174
+ return new TimelineImpl(options)
175
+ }
@@ -95,14 +95,19 @@ type FrameInstaller = (early: PhaseFn, late: PhaseFn) => boolean
95
95
  // The FIXED-phase source: wire `fixed` to the engine's per-substep hook and return true; false when the
96
96
  // host has no hook (older builds, 2D-only engines) -> the dispatcher steps its own 1/60 accumulator.
97
97
  type FixedInstaller = (fixed: PhaseFn) => boolean
98
- let frameInstaller: FrameInstaller | undefined
99
- let fixedInstaller: FixedInstaller | undefined
100
- /** @internal Install a render-synced frame source for aspect update(dt) (+ the fixed-step source). Called from an engine layer.
101
- * The FIRST engine layer wins: a 3D game that later builds a Scene2D for a UIImage minimap keeps its dispatch on the
102
- * 3D engine (the one it presents); the 2D engine's phases stay unwired there, so nothing ticks twice. */
103
- export const _installAspectFrames = (fn: FrameInstaller, fixed?: FixedInstaller): void => {
104
- if (frameInstaller) return
105
- frameInstaller = fn; fixedInstaller = fixed
98
+ // An engine layer's frame source. `presented`: a scene of THIS engine is the destination on show.
99
+ type FrameSource = { install: FrameInstaller, fixed?: FixedInstaller, presented?: () => boolean }
100
+ const registered: FrameSource[] = []
101
+ /** @internal Install a render-synced frame source for aspect update(dt) (+ the fixed-step source). Called
102
+ * from an engine layer, each layer once; EVERY layer's source is wired (at the dispatch's start, or now
103
+ * when it already runs), and the engine whose scene is on show drives the phases — see `drives`. A
104
+ * 3D game whose Scene2D is a UIImage minimap ticks on the 3D engine's frame; a project that opens a
105
+ * Scene2D and later a Scene (or the other way round) ticks on the frame of whichever is presented.
106
+ * Without `presented` (a test's source) the source always drives. */
107
+ export const _installAspectFrames = (install: FrameInstaller, fixed?: FixedInstaller, presented?: () => boolean): void => {
108
+ const src: FrameSource = { install, fixed, presented }
109
+ registered.push(src)
110
+ if (dispatchInstalled) wireSource(src)
106
111
  }
107
112
 
108
113
  /** @internal Engine-level phase hooks (the Net system, sdk/src/net): FIRST in the early and fixed
@@ -246,7 +251,6 @@ export const _flushDirtyAspects = (): void => drainDirty()
246
251
 
247
252
  let dispatchInstalled = false
248
253
  let fixedInstalled = false
249
- let fixedFallbackOn = false
250
254
  let fixedFallbackAcc = 0
251
255
 
252
256
  // The FIXED phase: dt = Time.fixedDt (1/60) every call, once per physics substep, on every host. The
@@ -276,7 +280,6 @@ const early: PhaseFn = (dt) => {
276
280
  drainDirty()
277
281
  earlyUpdaters.run("updateBefore", Time.dt)
278
282
  drainDirty()
279
- if (fixedFallbackOn) fixedFallback()
280
283
  }
281
284
  const late: PhaseFn = (dt) => {
282
285
  _phaseStamp.value++ // the physics step + transform sync ran since the early pass
@@ -286,12 +289,50 @@ const late: PhaseFn = (dt) => {
286
289
  /** @internal Tests: the phase passes themselves, whatever frame source is installed. */
287
290
  export const _runPhase = { early, late }
288
291
 
292
+ // A wired source: its engine fires `early` / `late` (and `fixed` per substep when `fixedWired`).
293
+ type Wired = { src: FrameSource, fixedWired: boolean }
294
+ const wired: Wired[] = []
295
+ // Which engine's frame runs the phases: the one whose scene is on show. An engine fires its phases
296
+ // only while it renders — the 3D engine while a 3D scene is presented, the 2D engine while a 2D
297
+ // scene is presented OR a UIImage(scene2d) node keeps it stepping — so with a 3D scene on show and a
298
+ // 2D minimap alive both fire, and only the 3D's count (the one in lockstep with the draw). With no
299
+ // scene of a wired engine on show (a screen over a paused game) whoever fires drives, so a minimap's
300
+ // aspects keep ticking under a menu. A source without `presented` (a test's) always drives.
301
+ const drives = (w: Wired): boolean => {
302
+ const presented = w.src.presented
303
+ if (presented === undefined || presented()) return true
304
+ return !wired.some(o => o.src.presented !== undefined && o.src.presented())
305
+ }
306
+ // The fixed phase of a frame whose engine has no substep hook: the SDK's own accumulator, right after
307
+ // the early phase (only once an `updateFixed` aspect exists — ensureFixedDispatch).
308
+ const wireFixed = (w: Wired): void => {
309
+ const fixed = w.src.fixed
310
+ if (fixed !== undefined && fixed((dt) => { if (drives(w)) fixedStep(dt) })) w.fixedWired = true
311
+ }
312
+ const wireSource = (src: FrameSource): void => {
313
+ const w: Wired = { src, fixedWired: false }
314
+ const ok = src.install(
315
+ (dt) => { if (drives(w)) { early(dt); if (fixedInstalled && !w.fixedWired) fixedFallback() } },
316
+ (dt) => { if (drives(w)) late(dt) },
317
+ )
318
+ if (!ok) return // this host has no hooks for that engine: not a source
319
+ wired.push(w)
320
+ if (fixedInstalled) wireFixed(w)
321
+ }
322
+
289
323
  const ensureDispatch = (): void => {
290
324
  if (dispatchInstalled) return
291
325
  dispatchInstalled = true
292
- // render-synced source if an engine layer installed one; else one host loop (early then late).
293
- if (!(frameInstaller && frameInstaller(early, late))) {
294
- setLoop((dt) => { early(dt); late(dt) })
326
+ // every engine layer's source; with none wired (a runtime without the hooks, a scene-less app's Net
327
+ // system) one host loop, early then late — which stands down the moment an engine's source is wired.
328
+ for (const src of registered) wireSource(src)
329
+ if (wired.length === 0) {
330
+ setLoop((dt) => {
331
+ if (wired.length > 0) return
332
+ early(dt)
333
+ if (fixedInstalled) fixedFallback()
334
+ late(dt)
335
+ })
295
336
  }
296
337
  }
297
338
  // Registered lazily - an app with no `updateFixed` never makes the engine call into JS per substep.
@@ -299,7 +340,7 @@ const ensureFixedDispatch = (): void => {
299
340
  if (fixedInstalled) return
300
341
  fixedInstalled = true
301
342
  ensureDispatch()
302
- if (!(fixedInstaller && fixedInstaller(fixedStep))) fixedFallbackOn = true
343
+ for (const w of wired) wireFixed(w)
303
344
  }
304
345
 
305
346
  /** @internal Force the frame dispatch on (the Net system pumps from the phase hooks even in an app
package/src/g2/Scene2D.ts CHANGED
@@ -26,7 +26,7 @@ const installRenderSyncedFrames = (): void => {
26
26
  _creator2d.setEarlyUpdate(early)
27
27
  _creator2d.setLateUpdate(late)
28
28
  return true
29
- })
29
+ }, undefined, () => Presentable.current instanceof Scene2D) // this engine drives the phases while a 2D scene is on show (and under a screen: the 3D engine is not rendering then)
30
30
  // Time.scale / Time.paused reach the engine's physics + animation clocks (feature-detected).
31
31
  _installTimeScale((scale) => { _creator2d.setTimeScale?.(scale) })
32
32
  }
@@ -1,52 +1,52 @@
1
- // CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
2
- // a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
3
- // side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
4
- // empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
5
- //
6
- // • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
7
- // `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
8
- // • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
9
- // owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
10
- // or anchors on it (`inst.alignTo(inst.nodes.eye)`).
11
- // • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
12
- // releases.
13
- // • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
14
- // See docs/scene-camera-place-plan.md.
15
-
16
- import { Aspect } from "../core/Aspect"
17
- import type { FieldMeta } from "../core/fields"
18
- import { Gizmos } from "../scene/gizmos"
19
- import type { Camera } from "./Camera"
20
- import type { Node } from "./Node"
21
-
22
- export class CameraPlace extends Aspect<"cameraPlace", Node> {
23
- static readonly aspect = "cameraPlace"
24
- /** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
25
- fov = 60
26
- /** Near clip distance. */
27
- near = 0.01
28
- /** Far clip distance = view range. */
29
- far = 1000
30
- /** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
31
- * ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
32
- active = false
33
- /** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
34
- static fields: FieldMeta<CameraPlace> = {
35
- fov: { label: "FOV", min: 1, max: 179, step: 1 },
36
- near: { min: 0.001, max: 10, step: 0.01 },
37
- far: { min: 1, max: 100000, step: 1 },
38
- active: { label: "Active camera" },
39
- }
40
- static editor = { rebuild: true }
41
-
42
- /** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
43
- applyTo(camera: Camera): void {
44
- camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
45
- }
46
-
47
- /** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
48
- * clicking it selects the node. Play mode never calls this. */
49
- rebuild(): void {
50
- Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
51
- }
52
- }
1
+ // CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
2
+ // a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
3
+ // side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
4
+ // empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
5
+ //
6
+ // • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
7
+ // `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
8
+ // • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
9
+ // owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
10
+ // or anchors on it (`inst.alignTo(inst.nodes.eye)`).
11
+ // • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
12
+ // releases.
13
+ // • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
14
+ // See docs/scene-camera-place-plan.md.
15
+
16
+ import { Aspect } from "../core/Aspect"
17
+ import type { FieldMeta } from "../core/fields"
18
+ import { Gizmos } from "../scene/gizmos"
19
+ import type { Camera } from "./Camera"
20
+ import type { Node } from "./Node"
21
+
22
+ export class CameraPlace extends Aspect<"cameraPlace", Node> {
23
+ static readonly aspect = "cameraPlace"
24
+ /** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
25
+ fov = 60
26
+ /** Near clip distance. */
27
+ near = 0.01
28
+ /** Far clip distance = view range. */
29
+ far = 1000
30
+ /** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
31
+ * ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
32
+ active = false
33
+ /** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
34
+ static fields: FieldMeta<CameraPlace> = {
35
+ fov: { label: "FOV", min: 1, max: 179, step: 1 },
36
+ near: { min: 0.001, max: 10, step: 0.01 },
37
+ far: { min: 1, max: 100000, step: 1 },
38
+ active: { label: "Active camera" },
39
+ }
40
+ static editor = { rebuild: true }
41
+
42
+ /** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
43
+ applyTo(camera: Camera): void {
44
+ camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
45
+ }
46
+
47
+ /** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
48
+ * clicking it selects the node. Play mode never calls this. */
49
+ rebuild(): void {
50
+ Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
51
+ }
52
+ }