lecodes-cli 0.17.1 → 0.18.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/index.js +1340 -648
- package/package.json +3 -3
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/bundler.ts +1 -1
- package/runtime/sdk/compile/compileProject.ts +16 -1
- package/runtime/sdk/compile/header.ts +6 -1
- package/runtime/sdk/compile/index.ts +16 -0
- package/runtime/sdk/compile/liteMaterial.ts +247 -0
- package/runtime/sdk/compile/shaderTargets.ts +42 -0
- package/runtime/sdk/core/Aspect.ts +255 -244
- package/runtime/sdk/g2/CharacterController2D.ts +2 -2
- package/runtime/sdk/gl/Camera.ts +41 -0
- package/runtime/sdk/gl/CameraPlace.ts +51 -0
- package/runtime/sdk/gl/CharacterController.ts +6 -2
- package/runtime/sdk/gl/IK.ts +193 -174
- package/runtime/sdk/gl/Material.ts +23 -0
- package/runtime/sdk/gl/Model.ts +7 -6
- package/runtime/sdk/gl/Node.ts +270 -285
- package/runtime/sdk/gl/Particles.ts +676 -625
- package/runtime/sdk/gl/Physics.ts +53 -1
- package/runtime/sdk/gl/Scene.ts +323 -291
- package/runtime/sdk/gl/Shape.ts +248 -8
- package/runtime/sdk/gl/Trigger.ts +50 -46
- package/runtime/sdk/gl/Vehicle.ts +519 -0
- package/runtime/sdk/gl/{AnimationClip.ts → animation/AnimationClip.ts} +37 -7
- package/runtime/sdk/gl/animation/Animator.ts +87 -0
- package/runtime/sdk/gl/animation/Layer.ts +29 -0
- package/runtime/sdk/gl/animation/Loop.ts +25 -0
- package/runtime/sdk/gl/animation/Playback.ts +43 -0
- package/runtime/sdk/gl/animation/core.ts +294 -0
- package/runtime/sdk/gl/scenarios.ts +26 -58
- package/runtime/sdk/inject.ts +18 -9
- package/runtime/sdk/runtime/app.ts +13 -0
- package/runtime/sdk/runtime/input.ts +169 -6
- package/runtime/sdk/scene/defineScene.ts +182 -26
- package/runtime/sdk/scene/gizmos.ts +128 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/gl/Animator.ts +0 -642
- package/runtime/sdk/gl/ModelAnimation.ts +0 -95
package/runtime/sdk/gl/Scene.ts
CHANGED
|
@@ -1,291 +1,323 @@
|
|
|
1
|
-
// A 3D scene: a draw set, a Camera, lights and environment. Opening a scene asks the host to bring up
|
|
2
|
-
// the 3D engine; only the active scene renders. Grown from the Phase 0 seed. Ported from
|
|
3
|
-
// worker/src/Scene.ts (Scene + ARScene), with the camera exposed as scene.camera.
|
|
4
|
-
//
|
|
5
|
-
// Lights are explicit: a scene gets image-based ambient (IBL) by default; add a Light.sun() for
|
|
6
|
-
// direct light + shadows.
|
|
7
|
-
|
|
8
|
-
import { Color, type ColorInput } from "../core/color"
|
|
9
|
-
import { _installAspectFrames } from "../core/Aspect"
|
|
10
|
-
import { _bumpNavEpoch, _navEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
|
|
11
|
-
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
12
|
-
import type { FetchResponse } from "../runtime/fetch"
|
|
13
|
-
import { _requestCameraPermission } from "../plugins/permission"
|
|
14
|
-
import { Camera } from "./Camera"
|
|
15
|
-
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
|
|
16
|
-
import { Material } from "./Material"
|
|
17
|
-
import { Node, nodeRegistry } from "./Node"
|
|
18
|
-
import { glState } from "./state"
|
|
19
|
-
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
20
|
-
|
|
21
|
-
export type SceneOptions = {
|
|
22
|
-
/** Image-based ambient lighting (default on). */
|
|
23
|
-
ibl?: boolean
|
|
24
|
-
/** IBL intensity (default 20000). */
|
|
25
|
-
environmentIntensity?: number
|
|
26
|
-
/** Bloom post-processing. */
|
|
27
|
-
bloom?: boolean
|
|
28
|
-
bloomIntensity?: number
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
_creator.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
1
|
+
// A 3D scene: a draw set, a Camera, lights and environment. Opening a scene asks the host to bring up
|
|
2
|
+
// the 3D engine; only the active scene renders. Grown from the Phase 0 seed. Ported from
|
|
3
|
+
// worker/src/Scene.ts (Scene + ARScene), with the camera exposed as scene.camera.
|
|
4
|
+
//
|
|
5
|
+
// Lights are explicit: a scene gets image-based ambient (IBL) by default; add a Light.sun() for
|
|
6
|
+
// direct light + shadows.
|
|
7
|
+
|
|
8
|
+
import { Color, type ColorInput } from "../core/color"
|
|
9
|
+
import { _installAspectFrames } from "../core/Aspect"
|
|
10
|
+
import { _bumpNavEpoch, _navEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
|
|
11
|
+
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
12
|
+
import type { FetchResponse } from "../runtime/fetch"
|
|
13
|
+
import { _requestCameraPermission } from "../plugins/permission"
|
|
14
|
+
import { Camera } from "./Camera"
|
|
15
|
+
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
|
|
16
|
+
import { Material } from "./Material"
|
|
17
|
+
import { Node, nodeRegistry } from "./Node"
|
|
18
|
+
import { glState } from "./state"
|
|
19
|
+
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
20
|
+
|
|
21
|
+
export type SceneOptions = {
|
|
22
|
+
/** Image-based ambient lighting (default on). */
|
|
23
|
+
ibl?: boolean
|
|
24
|
+
/** IBL intensity (default 20000). */
|
|
25
|
+
environmentIntensity?: number
|
|
26
|
+
/** Bloom post-processing. */
|
|
27
|
+
bloom?: boolean
|
|
28
|
+
bloomIntensity?: number
|
|
29
|
+
/** Tone mapping operator. `'aces'` (default, filament's ACES legacy) desaturates bright colours
|
|
30
|
+
* towards white — HDR fire reads pale; `'neutral'` (Khronos PBR Neutral) keeps hue and
|
|
31
|
+
* saturation until very bright; `'linear'` clips each channel (what an engine without a
|
|
32
|
+
* tonemapper shows — saturated, Unity-without-post-processing look); `'filmic'` (Uncharted). */
|
|
33
|
+
toneMapping?: "aces" | "neutral" | "linear" | "filmic"
|
|
34
|
+
/** Skybox / clear color. */
|
|
35
|
+
skybox?: ColorInput
|
|
36
|
+
/** Multisample anti-aliasing: `true` = 4×, `2`/`4` = that many samples, `false` = off (FXAA
|
|
37
|
+
* takes over). Unset keeps the host default (desktop 4×, mobile/web off). The biggest single
|
|
38
|
+
* fill-rate cost after resolution — turn it down on big screens before anything else. */
|
|
39
|
+
antialias?: boolean | 2 | 4
|
|
40
|
+
/** Render the 3D at this fraction of the viewport (0.25–1) and upscale; the UI stays at native
|
|
41
|
+
* resolution. A fixed, predictable cut of per-pixel GPU work — `0.75` is ~45 % cheaper and
|
|
42
|
+
* barely visible in motion, `0.5` quarters it. Headless renders ignore it. */
|
|
43
|
+
renderScale?: number
|
|
44
|
+
/** Let the engine shrink the 3D buffers under `renderScale` only when frames run over budget
|
|
45
|
+
* (Filament dynamic resolution, sharpened upscale) down to `min` (default 0.5). Off by default:
|
|
46
|
+
* it makes the output frame-time dependent, so flow tests / headless renders never enable it. */
|
|
47
|
+
dynamicResolution?: boolean | { min?: number }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Route aspect update(dt) through the render-synced native phases (render() early = before physics,
|
|
51
|
+
// late = after animations / before draw), so followers/cameras are in lockstep with the draw. Done from
|
|
52
|
+
// the 3D layer (not core) so a 2D-only bundle never pulls in the 3D bridge. Installed once, lazily on
|
|
53
|
+
// the first Scene. On an older wasm without the hooks the installer returns false → setLoop fallback.
|
|
54
|
+
let framesInstalled = false
|
|
55
|
+
const installRenderSyncedFrames = (): void => {
|
|
56
|
+
if (framesInstalled) return
|
|
57
|
+
framesInstalled = true
|
|
58
|
+
_installAspectFrames((early, late) => {
|
|
59
|
+
if (typeof _creator.setEarlyUpdate !== "function") return false
|
|
60
|
+
// the frame stamp invalidates every Node's local-matrix cache once per frame (physics / character
|
|
61
|
+
// controllers / animation rewrite transforms natively between frames)
|
|
62
|
+
_creator.setEarlyUpdate((dt: number) => { glState.frame++; early(dt) })
|
|
63
|
+
_creator.setLateUpdate(late)
|
|
64
|
+
return true
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class Scene implements Presentable {
|
|
69
|
+
/** @internal native scene handle. */
|
|
70
|
+
readonly _id: number
|
|
71
|
+
readonly camera: Camera
|
|
72
|
+
|
|
73
|
+
/** @internal touch listeners, read by the dispatch system. */
|
|
74
|
+
readonly _clickListeners: Array<(ev: ClickEvent<Node | null>) => void> = []
|
|
75
|
+
readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node | null>) => void> = []
|
|
76
|
+
|
|
77
|
+
private _material?: Material
|
|
78
|
+
|
|
79
|
+
private static _active: Scene | null = null
|
|
80
|
+
static get active(): Scene | null { return Scene._active }
|
|
81
|
+
|
|
82
|
+
constructor(options: SceneOptions = {}) {
|
|
83
|
+
installRenderSyncedFrames()
|
|
84
|
+
const parent = (options as any)._parent as Scene | undefined
|
|
85
|
+
if (parent) {
|
|
86
|
+
this.camera = parent.camera
|
|
87
|
+
this._id = _creator.createOverlayScene(parent._id)
|
|
88
|
+
// No camera re-attach: the native createOverlayScene wires the parent's camera into the
|
|
89
|
+
// overlay view. Re-attaching would (a) point camera._sceneId — the source for getRay /
|
|
90
|
+
// getViewDirection / fov — at the overlay's view, whose viewport/projection aren't
|
|
91
|
+
// maintained like the render view's, and (b) create a second native camera component on
|
|
92
|
+
// the shared entity. (Found via the scene editor: gizmo pick rays came out distorted.)
|
|
93
|
+
} else {
|
|
94
|
+
this.camera = new Camera()
|
|
95
|
+
this._id = _creator.createScene()
|
|
96
|
+
this.camera._attach(this._id)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.ibl !== false) _creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000)
|
|
100
|
+
if (options.bloom) _creator.setBloomOptions(this._id, true, options.bloomIntensity ?? 0.2, 1)
|
|
101
|
+
if (options.toneMapping !== undefined && options.toneMapping !== "aces") {
|
|
102
|
+
_creator.setToneMapping?.(this._id, { neutral: 1, linear: 2, filmic: 3 }[options.toneMapping] ?? 0)
|
|
103
|
+
}
|
|
104
|
+
if (options.skybox !== undefined) _creator.setSkybox(this._id, Color.toPackedRgb(options.skybox))
|
|
105
|
+
if (options.antialias !== undefined) {
|
|
106
|
+
const a = options.antialias
|
|
107
|
+
// `false` must reach the host: desktop defaults to 4× MSAA, so an explicit off is a real change.
|
|
108
|
+
_creator.setSceneMultiSampleAntiAliasing(this._id, a !== false, typeof a === "number" ? a : 4)
|
|
109
|
+
}
|
|
110
|
+
if (options.renderScale !== undefined || options.dynamicResolution) {
|
|
111
|
+
this.setRenderOptions(options.renderScale ?? 1, options.dynamicResolution ?? false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Runtime form of `renderScale` / `dynamicResolution` (a graphics-settings menu). `renderScale`
|
|
116
|
+
* 0.25–1; older hosts without the method ignore it. */
|
|
117
|
+
setRenderOptions(renderScale: number, dynamicResolution: boolean | { min?: number } = false): void {
|
|
118
|
+
const min = typeof dynamicResolution === "object" ? (dynamicResolution.min ?? 0.5) : 0.5
|
|
119
|
+
_creator.setSceneRenderOptions?.(this._id, renderScale, !!dynamicResolution, min)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Scene occlusion material. */
|
|
123
|
+
get occlusionMaterial(): Material {
|
|
124
|
+
if (!this._material) this._material = new Material(_creator.getSceneMaterial(this._id))
|
|
125
|
+
return this._material
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
set skybox(color: ColorInput) { _creator.setSkybox(this._id, Color.toPackedRgb(color)) }
|
|
129
|
+
|
|
130
|
+
setMaterialGlobalParameter(i: number, x: number, y: number, z: number, w: number): void {
|
|
131
|
+
_creator.setMaterialGlobalParameter(this._id, i, x, y, z, w)
|
|
132
|
+
}
|
|
133
|
+
setAntialias(enabled: boolean, scale = 4): void {
|
|
134
|
+
_creator.setSceneMultiSampleAntiAliasing(this._id, enabled, scale)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
add(...nodes: Node[]): this {
|
|
138
|
+
for (const n of nodes) _creator.addEntityToScene(this._id, n.id)
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
remove(...nodes: Node[]): this {
|
|
142
|
+
for (const n of nodes) _creator.removeEntityFromScene(this._id, n.id)
|
|
143
|
+
return this
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
createOverlay(options: SceneOptions = {}): Scene {
|
|
147
|
+
return new Scene({ ...options, _parent: this } as any)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
warmRender(): Promise<void> {
|
|
151
|
+
return new Promise<void>((res) => _creator.warmRender(this._id, res))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
155
|
+
// A scene is a destination like a screen: opening it replaces whatever is visible (suspending
|
|
156
|
+
// an active Router until Router.restore()), and it can be pushed onto the Router stack. The
|
|
157
|
+
// scene graph survives while covered — only the view detaches and rendering pauses.
|
|
158
|
+
|
|
159
|
+
/** @internal presentation lifecycle — fired by the host on present/dismiss (new navigation),
|
|
160
|
+
* or synchronously by open()/close() on legacy hosts. */
|
|
161
|
+
readonly ol: (() => void)[] = []
|
|
162
|
+
readonly cl: (() => void)[] = []
|
|
163
|
+
_backButtonCallback?: () => void
|
|
164
|
+
private _vd?: object
|
|
165
|
+
|
|
166
|
+
/** Fires when the scene becomes the visible destination (incl. a pop revealing it). */
|
|
167
|
+
onOpen(callback: () => void): this {
|
|
168
|
+
this.ol.push(callback)
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
/** Fires when the scene stops being visible (closed, replaced, or covered by a push). */
|
|
172
|
+
onClose(callback: () => void): this {
|
|
173
|
+
this.cl.push(callback)
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
onBackPressed(callback: () => void): this {
|
|
177
|
+
this._backButtonCallback = callback
|
|
178
|
+
return this
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
|
|
182
|
+
_viewDesc(): object {
|
|
183
|
+
return this._vd ??= { type: "scene3d", sceneId: this._id, _p: this }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Make this the active scene — shows it as the current destination (replaces a screen /
|
|
187
|
+
* another scene; only the active scene renders). */
|
|
188
|
+
open(options?: PresentOptions): void | Promise<void> {
|
|
189
|
+
_bumpNavEpoch()
|
|
190
|
+
if (_navSupported()) {
|
|
191
|
+
// The host activates the engine (openScene) as part of presenting the destination.
|
|
192
|
+
_creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
|
|
193
|
+
} else {
|
|
194
|
+
_creator.openScene(this._id)
|
|
195
|
+
for (const cb of this.ol) cb()
|
|
196
|
+
}
|
|
197
|
+
Scene._active = this
|
|
198
|
+
glState.activeScene = this
|
|
199
|
+
_setCurrent(this)
|
|
200
|
+
}
|
|
201
|
+
close(): void {
|
|
202
|
+
_bumpNavEpoch()
|
|
203
|
+
if (_navSupported()) {
|
|
204
|
+
_creatorUI.closeView!()
|
|
205
|
+
} else {
|
|
206
|
+
_creator.closeScene()
|
|
207
|
+
for (const cb of this.cl) cb()
|
|
208
|
+
}
|
|
209
|
+
if (Scene._active === this) Scene._active = null
|
|
210
|
+
if (glState.activeScene === this) glState.activeScene = null
|
|
211
|
+
if (Presentable.current === this) _setCurrent(null)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
addEventListener(channel: "click", callback: (ev: ClickEvent<Node | null>) => void): void
|
|
215
|
+
addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<Node | null>) => void): void
|
|
216
|
+
addEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
|
|
217
|
+
if (channel === "click") {
|
|
218
|
+
if (!this._clickListeners.includes(callback)) { this._clickListeners.push(callback); registerTouchEndEvent() }
|
|
219
|
+
} else if (channel === "touchstart") {
|
|
220
|
+
if (!this._touchStartListeners.includes(callback)) { this._touchStartListeners.push(callback); registerTouchStartEvent() }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
removeEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
|
|
224
|
+
const list = channel === "click" ? this._clickListeners : this._touchStartListeners
|
|
225
|
+
const i = list.indexOf(callback)
|
|
226
|
+
if (i >= 0) list.splice(i, 1)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- AR ---
|
|
231
|
+
|
|
232
|
+
export type ARMode = "default" | "markers" | "arcore" | "detached"
|
|
233
|
+
|
|
234
|
+
const onTrackStateChange = (entityId: number, track: boolean): void => {
|
|
235
|
+
const node = nodeRegistry.get(entityId)
|
|
236
|
+
if (!node) return
|
|
237
|
+
node.isTracked = track
|
|
238
|
+
if (track) node._dispatchEvent("track")
|
|
239
|
+
else node._dispatchEvent("untrack")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export class ARScene<T extends ARMode = "default"> extends Scene {
|
|
243
|
+
readonly mode: T
|
|
244
|
+
useWarmRender = true
|
|
245
|
+
private _root?: Node
|
|
246
|
+
|
|
247
|
+
constructor(options: SceneOptions & { mode?: T } = {}) {
|
|
248
|
+
super(options)
|
|
249
|
+
this.mode = options.mode ?? ("default" as T)
|
|
250
|
+
_creator.createARController(this._id, this.camera.id, this.mode, onTrackStateChange)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Root anchor node (not available in marker mode). */
|
|
254
|
+
get root(): Node {
|
|
255
|
+
if (this._root) return this._root
|
|
256
|
+
this._root = new Node(_creator.createRootAnchor(this._id))
|
|
257
|
+
return this._root
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Create an anchor from a tracking source (e.g. an image target). */
|
|
261
|
+
createAnchor(source: FetchResponse, physicalWidth = 0.2): Node {
|
|
262
|
+
return new Node(_creator.createAnchor(this._id, physicalWidth, (source as any)._id))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Let the user place & manipulate a node with touch gestures — the staple AR object-placement
|
|
267
|
+
* interaction: ONE finger drags it across the floor (relative to where the camera looks), TWO fingers
|
|
268
|
+
* pinch to scale and twist to rotate. Returns a handle; call `.remove()` to detach. Tunable via
|
|
269
|
+
* options (min/max scale, and toggles for `pan` / `pinch` / `twist`).
|
|
270
|
+
*/
|
|
271
|
+
addControls(target: Node, options: ControlsOptions = {}): ControlsHandle {
|
|
272
|
+
return attachControls(this, target, options)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
override async open(options?: PresentOptions): Promise<void> {
|
|
276
|
+
await this._prepare()
|
|
277
|
+
super.open(options)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @internal Presentable prepare hook: camera permission + warm render + AR session, all while
|
|
281
|
+
* the previous destination (typically a loading screen) stays visible — the swap to this scene
|
|
282
|
+
* happens only after it resolves. Warm render is headless on every platform (offscreen
|
|
283
|
+
* swapchain), so no GL view is created here — the host mounts the scene view at present time.
|
|
284
|
+
* Rejects on camera denial / AR launch failure, or if another navigation happened while
|
|
285
|
+
* preparing (superseded) — in every rejection case the current destination stays visible. */
|
|
286
|
+
async _prepare(): Promise<void> {
|
|
287
|
+
const epoch = _navEpoch()
|
|
288
|
+
await _requestCameraPermission()
|
|
289
|
+
if (this.useWarmRender) await this.warmRender()
|
|
290
|
+
await new Promise<void>((res, rej) => _creator.launchAR(this._id, res, rej))
|
|
291
|
+
if (_navEpoch() !== epoch) {
|
|
292
|
+
_creator.stopAR()
|
|
293
|
+
throw new Error("ARScene.open() superseded: another destination was opened while preparing")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
override close(): void {
|
|
298
|
+
_creator.stopAR()
|
|
299
|
+
super.close()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- VR ---
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* A 3D scene rendered in a VR headset (OpenXR — e.g. Meta Quest). The headset drives the camera:
|
|
307
|
+
* every frame both eyes are rendered from the tracked head pose, so `scene.camera` position/lookAt
|
|
308
|
+
* have no effect while the scene is open. The world origin is at floor level (stage space) —
|
|
309
|
+
* author content with the ground at y = 0 and the user standing at the origin.
|
|
310
|
+
*
|
|
311
|
+
* `open()` rejects on hosts without a VR runtime ("VR is not supported on this device").
|
|
312
|
+
*/
|
|
313
|
+
export class VRScene extends Scene {
|
|
314
|
+
override async open(options?: PresentOptions): Promise<void> {
|
|
315
|
+
await new Promise<void>((res, rej) => _creator.launchVR(this._id, res, rej))
|
|
316
|
+
super.open(options)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
override close(): void {
|
|
320
|
+
_creator.stopVR()
|
|
321
|
+
super.close()
|
|
322
|
+
}
|
|
323
|
+
}
|