lecodes-sdk 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +4 -0
- package/dist/types/gl/Foliage.d.ts +5 -0
- package/dist/types/gl/Lightmap.d.ts +5 -3
- package/dist/types/gl/Material.d.ts +6 -0
- package/dist/types/gl/Scatter.d.ts +25 -0
- package/dist/types/gl/terrain/Terrain.d.ts +5 -0
- package/dist/types/inject.d.ts +2 -0
- package/dist/types/runtime/device.d.ts +20 -0
- 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/audio/support.ts +3 -1
- package/src/bridges/audio.d.ts +23 -18
- package/src/bridges/device.d.ts +19 -0
- package/src/bridges/gl.d.ts +11 -0
- package/src/compile/serverSplit.ts +8 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +7 -0
- package/src/gl/Light.ts +257 -257
- package/src/gl/Lightmap.ts +8 -3
- package/src/gl/Material.ts +10 -1
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Scatter.ts +67 -0
- package/src/gl/animation/DynamicBone.ts +486 -486
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +17 -0
- package/src/inject.ts +2 -0
- package/src/runtime/device.ts +29 -0
- 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/audio/support.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Feature gate for the game audio system (docs/audio-plan.md). `_creatorAudio` is OPTIONAL as a
|
|
2
2
|
// block: a host without it (or whose engine failed to open a device) makes every Sound silent and
|
|
3
|
-
// every Voice inert — a project stays runnable, nothing throws.
|
|
3
|
+
// every Voice inert — a project stays runnable, nothing throws. hasSupport() opens no device: the
|
|
4
|
+
// native engine is created by the first call that needs it (a load, a bus write), so reading this
|
|
5
|
+
// at load costs nothing; a host whose device then fails to open hands Sound.load the silent clip.
|
|
4
6
|
|
|
5
7
|
export const audioSupported: boolean =
|
|
6
8
|
typeof _creatorAudio !== "undefined" && !!_creatorAudio && typeof _creatorAudio.hasSupport === "function" && _creatorAudio.hasSupport()
|
package/src/bridges/audio.d.ts
CHANGED
|
@@ -13,27 +13,32 @@ declare global {
|
|
|
13
13
|
// CREATOR_PKG_AUDIO (desktop first; Android / Apple pending — parity `audio-game`). The headless
|
|
14
14
|
// renderer provides a RECORDER (plays are logged, nothing is mixed). Every parameter block is a
|
|
15
15
|
// fixed-order Float32Array (creator-audio.h CAUD_SRC_* / the effect param orders).
|
|
16
|
+
// The engine (and the audio DEVICE) is created LAZILY, by the first call that needs it — the
|
|
17
|
+
// `@guard audioEnsureEngine` on every method that touches engine state (creator-pkg.h
|
|
18
|
+
// "creator-audio host integration"): an app that never makes a sound never opens the device.
|
|
19
|
+
// hasSupport() therefore opens nothing: it is "the bridge is here and no init has failed".
|
|
16
20
|
/** @engine audio */
|
|
17
21
|
var _creatorAudio: {
|
|
18
|
-
/**
|
|
22
|
+
/** True on a host with the bridge until an engine init has FAILED (no device at all). Opens
|
|
23
|
+
* nothing: the engine is created by the first call that needs it. @custom */
|
|
19
24
|
hasSupport(): boolean
|
|
20
25
|
/** Decode the bytes behind a fetch system id (WAV / MP3 / OGG Vorbis) on the engine's
|
|
21
26
|
* loader thread; mono unless `stereo`. onDone(clipId, durationSeconds, channels). @custom */
|
|
22
27
|
loadClip(systemId: bytes, stereo: boolean, onDone: (clipId: number, duration: number, channels: number) => void, onReject: (message: string) => void): void
|
|
23
28
|
/** @c caudClipRelease */
|
|
24
29
|
releaseClip(clipId: u32): void
|
|
25
|
-
/** Bus id by name (-1 unknown); 0 master, 1 sfx, 2 music, 3 ui, 4 voice. @c caudBusId */
|
|
30
|
+
/** Bus id by name (-1 unknown); 0 master, 1 sfx, 2 music, 3 ui, 4 voice. @guard audioEnsureEngine @c caudBusId */
|
|
26
31
|
busId(name: string): i32
|
|
27
|
-
/** Creates (or finds) an app-defined bus under master. -1 when the 16 slots are full. @c caudBusCreate */
|
|
32
|
+
/** Creates (or finds) an app-defined bus under master. -1 when the 16 slots are full. @guard audioEnsureEngine @c caudBusCreate */
|
|
28
33
|
createBus(name: string): i32
|
|
29
|
-
/** @c caudBusSetVolume */
|
|
34
|
+
/** @guard audioEnsureEngine @c caudBusSetVolume */
|
|
30
35
|
setBusVolume(bus: i32, volume: f32): void
|
|
31
|
-
/** @c caudBusSetMuted */
|
|
36
|
+
/** @guard audioEnsureEngine @c caudBusSetMuted */
|
|
32
37
|
setBusMuted(bus: i32, muted: boolean): void
|
|
33
38
|
/** kind 1 reverb [roomSize, damping, width, mix, preDelay] · 2 echo [delay, decay, mix] ·
|
|
34
|
-
* 3 lowpass [cutoffHz]; an empty array turns the effect off. Changes are smoothed. @c caudBusSetEffect */
|
|
39
|
+
* 3 lowpass [cutoffHz]; an empty array turns the effect off. Changes are smoothed. @guard audioEnsureEngine @c caudBusSetEffect */
|
|
35
40
|
setBusEffect(bus: i32, kind: i32, params: Float32Array): void
|
|
36
|
-
/** @c caudBusStopAll */
|
|
41
|
+
/** @guard audioEnsureEngine @c caudBusStopAll */
|
|
37
42
|
stopBus(bus: i32, fade: f32): void
|
|
38
43
|
/** A 3D emitter. `attachSource` binds it to an entity the engine follows every frame;
|
|
39
44
|
* `setSourcePosition` places an unbound one (playAt). @guard audioEnsureEngine @c caudSourceCreate */
|
|
@@ -41,11 +46,11 @@ declare global {
|
|
|
41
46
|
/** @c gl:audioAttachSource */
|
|
42
47
|
attachSource(sourceId: u32, entityId: u32): void
|
|
43
48
|
/** [minDistance, maxDistance, rolloff (0 none 1 inverse 2 linear 3 exp), coneInnerDeg,
|
|
44
|
-
* coneOuterDeg, coneOuterGain, doppler, spread, occlusion (0/1), bus]. @c caudSourceSetParams */
|
|
49
|
+
* coneOuterDeg, coneOuterGain, doppler, spread, occlusion (0/1), bus]. @guard audioEnsureEngine @c caudSourceSetParams */
|
|
45
50
|
setSourceParams(sourceId: u32, params: Float32Array): void
|
|
46
|
-
/** @c caudSourceSetPose @pack x 3 @rest nullptr */
|
|
51
|
+
/** @guard audioEnsureEngine @c caudSourceSetPose @pack x 3 @rest nullptr */
|
|
47
52
|
setSourcePosition(sourceId: u32, x: f32, y: f32, z: f32): void
|
|
48
|
-
/** @c caudSourceStopAll */
|
|
53
|
+
/** @guard audioEnsureEngine @c caudSourceStopAll */
|
|
49
54
|
stopSource(sourceId: u32, fade: f32): void
|
|
50
55
|
/** @c caudSourceVoices */
|
|
51
56
|
sourceVoices(sourceId: u32): i32
|
|
@@ -53,22 +58,22 @@ declare global {
|
|
|
53
58
|
destroySource(sourceId: u32): void
|
|
54
59
|
/** The listener entity; 0 = the active scene camera (the default). @c gl:audioSetListener */
|
|
55
60
|
setListener(entityId: u32): void
|
|
56
|
-
/** @c caudListenerSetDoppler */
|
|
61
|
+
/** @guard audioEnsureEngine @c caudListenerSetDoppler */
|
|
57
62
|
setListenerOptions(dopplerFactor: f32): void
|
|
58
63
|
/** HRTF binaural rendering for the `maxVoices` nearest spatial voices (headphones); the rest keep panning. @guard audioEnsureEngine @c caudSetHrtf */
|
|
59
64
|
setHrtf(enabled: boolean, maxVoices: i32): void
|
|
60
65
|
/** Whether the engine's time scale (Time.scale) also pitches the sfx bus. Default false (pause = mute only). @guard audioEnsureEngine @c caudSetTimeScalePitch */
|
|
61
66
|
setTimeScalePitch(enabled: boolean): void
|
|
62
67
|
/** → a voice token (0 = nothing played: the pool refused, the clip is not ready). bus -1 = the
|
|
63
|
-
* source's bus (sfx for 2D). pan is 2D only. @c caudPlay @pack bus CaudPlay */
|
|
68
|
+
* source's bus (sfx for 2D). pan is 2D only. @guard audioEnsureEngine @c caudPlay @pack bus CaudPlay */
|
|
64
69
|
play(clipId: u32, sourceId: u32, bus: i32, volume: f32, pitch: f32, loop: boolean, priority: i32, fadeIn: f32, startAt: f32, pan: f32): u32
|
|
65
|
-
/** @c caudVoiceSetVolume */
|
|
70
|
+
/** @guard audioEnsureEngine @c caudVoiceSetVolume */
|
|
66
71
|
setVoiceVolume(token: u32, volume: f32): void
|
|
67
|
-
/** @c caudVoiceSetPitch */
|
|
72
|
+
/** @guard audioEnsureEngine @c caudVoiceSetPitch */
|
|
68
73
|
setVoicePitch(token: u32, pitch: f32): void
|
|
69
|
-
/** @c caudVoiceSetPan */
|
|
74
|
+
/** @guard audioEnsureEngine @c caudVoiceSetPan */
|
|
70
75
|
setVoicePan(token: u32, pan: f32): void
|
|
71
|
-
/** @c caudVoiceStop */
|
|
76
|
+
/** @guard audioEnsureEngine @c caudVoiceStop */
|
|
72
77
|
stopVoice(token: u32, fade: f32): void
|
|
73
78
|
/** @c caudVoicePlaying */
|
|
74
79
|
voicePlaying(token: u32): boolean
|
|
@@ -77,7 +82,7 @@ declare global {
|
|
|
77
82
|
/** THE ended listener (single slot per JS world): called once per tick with the tokens of the
|
|
78
83
|
* voices that ended since the previous tick — natural end, stop, steal. @custom */
|
|
79
84
|
setOnEnded(callback: ((tokens: Float32Array) => void) | null): void
|
|
80
|
-
/** @c caudStopAll */
|
|
85
|
+
/** @guard audioEnsureEngine @c caudStopAll */
|
|
81
86
|
stopAll(fade: f32): void
|
|
82
87
|
/** A reverb volume: shape 0 box (dims = full size) / 1 sphere (dims[0] = diameter), in the
|
|
83
88
|
* entity's local units; `blend` metres of crossfade inside the border; reverb params as for
|
|
@@ -85,7 +90,7 @@ declare global {
|
|
|
85
90
|
createZone(): u32
|
|
86
91
|
/** @c gl:audioAttachZone */
|
|
87
92
|
attachZone(zoneId: u32, entityId: u32): void
|
|
88
|
-
/** @c caudZoneSet */
|
|
93
|
+
/** @guard audioEnsureEngine @c caudZoneSet */
|
|
89
94
|
setZone(zoneId: u32, shape: i32, dims: Float32Array, blend: f32, bus: i32, reverb: Float32Array): void
|
|
90
95
|
/** @c gl:audioDetachZone, caudZoneDestroy */
|
|
91
96
|
destroyZone(zoneId: u32): void
|
package/src/bridges/device.d.ts
CHANGED
|
@@ -38,6 +38,25 @@ declare global {
|
|
|
38
38
|
/** Whether the overlay is on. */
|
|
39
39
|
statsOverlay?(): boolean
|
|
40
40
|
|
|
41
|
+
// ---- The frame rate (device.setFrameRate / setVsync / displayRefreshRate) ---------------------
|
|
42
|
+
/** Cap the host's frame loop at `fps` frames a second (device.setFrameRate): the tick, and the
|
|
43
|
+
* 3D frame with it, runs at most that often. `0` = no cap: the display's own rate, or, with
|
|
44
|
+
* vsync off, as fast as the host renders. A request, not a state — a phone asks its panel for
|
|
45
|
+
* the rate and may be refused; the screen's own maximum still wins. Host state, kept through
|
|
46
|
+
* `_creatorApp.run()`. Optional: a host whose loop has no pacing of its own (web, headless)
|
|
47
|
+
* leaves it out. */
|
|
48
|
+
setFrameRate?(fps: f64): void
|
|
49
|
+
/** Present without waiting for the display's refresh (device.setVsync): off = no vsync wait,
|
|
50
|
+
* tearing possible, and with no cap set the loop renders as fast as it can. Optional: desktop
|
|
51
|
+
* only — a mobile compositor never presents outside the refresh, so the slot is absent there
|
|
52
|
+
* and the SDK answers `false`. */
|
|
53
|
+
setVsync?(enabled: boolean): void
|
|
54
|
+
/** The display's refresh rate in Hz (device.displayRefreshRate): the fastest the screen offers
|
|
55
|
+
* the app — 120 on a 120 Hz phone whatever mode the cap has put it in (an iPhone's 60 unless
|
|
56
|
+
* the app's Info.plist lifts the floor), the monitor's current mode on desktop. Optional: 0
|
|
57
|
+
* where the host cannot tell (web, Linux for now). */
|
|
58
|
+
displayRefreshRate?(): f64
|
|
59
|
+
|
|
41
60
|
// ---- device.motion (orientation sensor; pull-per-frame model) --------------------------------
|
|
42
61
|
/** Does this device have the motion sensors (gyro)? Surfaced as device.motion.available. */
|
|
43
62
|
motionAvailable?(): boolean
|
package/src/bridges/gl.d.ts
CHANGED
|
@@ -286,6 +286,11 @@ declare global {
|
|
|
286
286
|
setInstanceTransforms(entityId: u32, matrices: Float32Array, first: u32): void
|
|
287
287
|
setInstancedMeshMaterial(entityId: u32, materialId: u32): void
|
|
288
288
|
setInstancedMeshShadows(entityId: u32, cast: boolean, receive: boolean): void
|
|
289
|
+
/** GLB scatter (gl/Scatter.ts ↔ creator-gl/src/scatter.cpp): the loaded model `sourceId` drawn at `placements.length / 5`
|
|
290
|
+
* placements (x y z, yaw in degrees, uniform scale) under `entityId`, hardware-instanced in cells of `cell` metres
|
|
291
|
+
* with the source's own buffers and material instances. Returns the renderables made (0 = nothing scattered).
|
|
292
|
+
* Absent on a host without it: Scatter then clones the model per placement. @count count=placements/5 */
|
|
293
|
+
createGlbScatter?(entityId: u32, sourceId: u32, placements: Float32Array, cell: f32, castShadows: boolean): u32
|
|
289
294
|
setCulling(entityId: u32, culling: boolean): void
|
|
290
295
|
/** Filament RenderableManager::setPriority — the coarse draw order, 0..7 (Mesh.renderPriority).
|
|
291
296
|
* Optional: an older host leaves everything at the default 4. */
|
|
@@ -780,6 +785,12 @@ declare global {
|
|
|
780
785
|
/** Distance fade for every instance of the entity's ASSET (present and future): the cards thin out between `start`
|
|
781
786
|
* and `end` metres from the camera and past `end` the LOD pass takes the instance out of the scene. end 0 = none. */
|
|
782
787
|
foliageSetFade?(entityId: u32, start: f32, end: f32): void
|
|
788
|
+
/** THE PLANTS' CASTER DISTANCE: a foliage instance casts the sun's shadow only within `distance` metres of the camera
|
|
789
|
+
* (the LOD pass flips its renderables' castShadows); past it a baked ground keeps the shadow. 0 = every plant casts. */
|
|
790
|
+
foliageSetCast?(distance: f32): void
|
|
791
|
+
/** THE GROUND'S COLOUR UNDER THE PLANTS: the terrain's control map (RGBA = its four layers' weights) and each layer's
|
|
792
|
+
* mean colour (12 floats, linear rgb x 4) — a grass blade's base takes the colour of the soil it stands in. Level-wide. */
|
|
793
|
+
foliageSetGroundTint?(terrainId: u32, controlTextureId: u32, colors: Float32Array): void
|
|
783
794
|
/** @ifdef CREATOR_GL_PHYSICS */
|
|
784
795
|
physicsConfigure(gx: f32, gy: f32, gz: f32, maxBodies: u32): void
|
|
785
796
|
/** @c setPhysicsInterpolation @ifdef CREATOR_GL_PHYSICS */
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { AppConfig } from "../server/auth/appConfig"
|
|
16
|
+
import { CODE_EXT } from "./assetMacro"
|
|
16
17
|
import { bundleProjectWithMap, SERVER_INJECT, type MemoryFiles } from "./bundler"
|
|
17
18
|
import type { CompileEntry } from "./compileProject"
|
|
18
19
|
import { deriveEndpointParams } from "./serverTypes"
|
|
@@ -271,12 +272,18 @@ export const compileServerBundle = async (opts: CompileServerOptions): Promise<{
|
|
|
271
272
|
// with a Figma-exported design next to app.server.ts). A stub's value is the resource's PATH: a
|
|
272
273
|
// row of shared data that names a picture (`photo: "images/a.jpg"`) reaches the server as the
|
|
273
274
|
// path of that picture, which a seed stores into a file field (files/db.ts). The board's `design/`
|
|
274
|
-
// files are never server code and stay out entirely.
|
|
275
|
+
// files are never server code and stay out entirely. A text file that is not code follows the
|
|
276
|
+
// app bundle's rules (compileProject.ts entriesToFiles): a `.json` is its parsed data — a shared
|
|
277
|
+
// module of a price list the person gave does `import raw from "./price.json"`, and the seed
|
|
278
|
+
// that imports the module ran into "raw is not defined" while the text went in as code — and any
|
|
279
|
+
// other text (.csv, .txt, .md) is its string.
|
|
275
280
|
const files: MemoryFiles = {}
|
|
276
281
|
for (const e of opts.entries) {
|
|
277
282
|
const path = normalizePath(e.path)
|
|
278
283
|
if (/^\/design\//.test(path)) continue
|
|
279
284
|
if (e.type !== "text" || path.toLowerCase().endsWith(".svg")) { files[path] = `export default ${JSON.stringify(path)}`; continue }
|
|
285
|
+
if (path.toLowerCase().endsWith(".json")) { files[path] = `export default ${e.text?.trim() || "null"}`; continue }
|
|
286
|
+
if (!CODE_EXT.test(path)) { files[path] = `export default ${JSON.stringify(e.text ?? "")}`; continue }
|
|
280
287
|
files[path] = e.text ?? ""
|
|
281
288
|
}
|
|
282
289
|
let params = opts.params
|