lecodes-sdk 2.4.2 → 2.4.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/dist/types/plugins/gen/ar/contract.d.ts +5 -5
- package/dist/types/plugins/gen/ar/sdk/ar.gen.d.ts +2 -2
- package/dist/types/plugins/gen/browser/sdk/browser.d.ts +2 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Light.ts +257 -257
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/scenarios.ts +291 -291
- package/src/plugins/gen/ar/contract.d.ts +5 -5
- package/src/plugins/gen/ar/sdk/ar.gen.ts +2 -2
- package/src/plugins/gen/browser/sdk/browser.ts +11 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/gl/CameraPlace.ts
CHANGED
|
@@ -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
|
+
}
|