lecodes-sdk 0.20.2 → 1.1.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 +35 -9
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +36 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -51
- package/dist/types/gl/Material.d.ts +32 -20
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +41 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +53 -1
- package/dist/types/gl/Scene.d.ts +21 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
- package/dist/types/gl/animation/IK.d.ts +109 -0
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/physics/Ragdoll.d.ts +161 -0
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +10 -8
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +35 -27
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +50 -31
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +1760 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +3 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +109 -0
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +440 -249
- package/src/gl/Material.ts +69 -36
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +167 -124
- package/src/gl/Node.ts +40 -1
- package/src/gl/Particles.ts +82 -5
- package/src/gl/Scene.ts +35 -2
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -0
- package/src/gl/animation/IK.ts +214 -0
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/physics/Ragdoll.ts +451 -0
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +14 -12
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +236 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +92 -66
- package/src/scene/level.ts +2 -2
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/IK.d.ts +0 -53
- package/dist/types/gl/Ragdoll.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/animate/animate.ts +0 -238
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/IK.ts +0 -193
- package/src/gl/Ragdoll.ts +0 -270
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/gl/Model.ts
CHANGED
|
@@ -1,124 +1,167 @@
|
|
|
1
|
-
// A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
|
|
2
|
-
// distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
|
|
3
|
-
// model.anim (an Animator over the GLB's clips — crossfades, blend spaces, layers when you need them):
|
|
4
|
-
// const hero = await Model.load(asset('./hero.glb'))
|
|
5
|
-
// hero.anim.play('Run', { loop: true })
|
|
6
|
-
|
|
7
|
-
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
8
|
-
import { Node } from "./Node"
|
|
9
|
-
import { Animator, _pushLod, type LodMode } from "./animation/Animator"
|
|
10
|
-
import { Material } from "./Material"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
get
|
|
60
|
-
set
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
1
|
+
// A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
|
|
2
|
+
// distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
|
|
3
|
+
// model.anim (an Animator over the GLB's clips — crossfades, blend spaces, layers when you need them):
|
|
4
|
+
// const hero = await Model.load(asset('./hero.glb'))
|
|
5
|
+
// hero.anim.play('Run', { loop: true })
|
|
6
|
+
|
|
7
|
+
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
8
|
+
import { Node } from "./Node"
|
|
9
|
+
import { Animator, _pushLod, type LodMode } from "./animation/Animator"
|
|
10
|
+
import { Material } from "./Material"
|
|
11
|
+
|
|
12
|
+
/** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
|
|
13
|
+
* JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
|
|
14
|
+
* `world` its skinned position this frame. */
|
|
15
|
+
export interface TrianglePick {
|
|
16
|
+
/** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
|
|
17
|
+
entity: number
|
|
18
|
+
node: number
|
|
19
|
+
nodeName: string
|
|
20
|
+
mesh: string
|
|
21
|
+
primitive: number
|
|
22
|
+
material: string
|
|
23
|
+
/** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
|
|
24
|
+
triangle: number
|
|
25
|
+
backface: boolean
|
|
26
|
+
distance: number
|
|
27
|
+
point: [number, number, number]
|
|
28
|
+
bary: [number, number, number]
|
|
29
|
+
skin: string
|
|
30
|
+
vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Model extends Node {
|
|
34
|
+
/** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
|
|
35
|
+
* more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
|
|
36
|
+
declare readonly anim: Animator
|
|
37
|
+
/** @internal loaded through the lightmap material (a baked static); clones inherit it. */
|
|
38
|
+
_lightmapped = false
|
|
39
|
+
/** @internal loaded through the foliage tier (`Foliage`); clones inherit it. */
|
|
40
|
+
_foliage = false
|
|
41
|
+
/** @internal the shadow pair, mirrored here because the bridge writes both at once. */
|
|
42
|
+
_castShadows = true
|
|
43
|
+
/** @internal */
|
|
44
|
+
_receiveShadows = true
|
|
45
|
+
/** @internal */
|
|
46
|
+
_culling = true
|
|
47
|
+
/** @internal -1 = auto (see `lod`) */
|
|
48
|
+
_lodMesh = -1
|
|
49
|
+
|
|
50
|
+
constructor(internalId: number) {
|
|
51
|
+
super(internalId)
|
|
52
|
+
this.aspect(Animator)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Does this model cast a real-time shadow? Unlike Mesh (one renderable) a GLB is a whole
|
|
56
|
+
* hierarchy, so the flag goes to EVERY renderable of the instance. A first-person viewmodel —
|
|
57
|
+
* arms, weapon, attachments — sets it false: it lives in front of the camera and its shadow
|
|
58
|
+
* is never wanted. */
|
|
59
|
+
get castShadows(): boolean { return this._castShadows }
|
|
60
|
+
set castShadows(v: boolean) { this._castShadows = v; this._applyShadows() }
|
|
61
|
+
|
|
62
|
+
/** Is this model lit by other casters' shadows? Same instance-wide reach as castShadows. */
|
|
63
|
+
get receiveShadows(): boolean { return this._receiveShadows }
|
|
64
|
+
set receiveShadows(v: boolean) { this._receiveShadows = v; this._applyShadows() }
|
|
65
|
+
|
|
66
|
+
/** Frustum culling for the whole instance: a model the camera cannot see skips the draw and the
|
|
67
|
+
* shadow pass. ON by default where the host keeps a skinned mesh's bounds honest — the engine refits
|
|
68
|
+
* them to the joints every frame, so a walking, kneeling or ragdolled body is never culled while on
|
|
69
|
+
* screen (`_creator.skinnedCullingSupported`); OFF (always draw) on hosts without that, where
|
|
70
|
+
* Filament would cull an animated body by its bind-pose box. `false` = always draw (a skybox-sized
|
|
71
|
+
* mesh, a debugging aid); `true` forces it on regardless of the host. */
|
|
72
|
+
get culling(): boolean { return this._culling }
|
|
73
|
+
set culling(v: boolean) { this._culling = v; _creator.setGlbCulling(this.id, v) }
|
|
74
|
+
/** Level of detail (docs/lod-plan.md). `'auto'` (default): the engine shows the `_LOD<n>` mesh level
|
|
75
|
+
* that fits the model's size on screen (`lecodes assets doctor --lod` makes them) and scales the
|
|
76
|
+
* animation rate with it; a number 0–3 pins that level for both — `0` = always full detail (a hero,
|
|
77
|
+
* a showcase), `2`/`3` = always cheap (a crowd filler). `model.anim.lod = 'full'` keeps the animation
|
|
78
|
+
* exact while the mesh still switches. No-op on hosts without the LOD pass. */
|
|
79
|
+
get lod(): LodMode { return this._lodMesh < 0 ? "auto" : (this._lodMesh as 0 | 1 | 2 | 3) }
|
|
80
|
+
set lod(v: LodMode) { this._lodMesh = v === "auto" ? -1 : Math.max(0, Math.min(3, Math.round(v))); _pushLod(this) }
|
|
81
|
+
|
|
82
|
+
/** @internal the default for a fresh instance (load / clone), decided once per host. */
|
|
83
|
+
static _cullingDefault(): boolean {
|
|
84
|
+
if (Model._cullDefault === undefined) Model._cullDefault = !!_creator.skinnedCullingSupported?.()
|
|
85
|
+
return Model._cullDefault
|
|
86
|
+
}
|
|
87
|
+
private static _cullDefault: boolean | undefined
|
|
88
|
+
|
|
89
|
+
/** @internal both flags travel together — the bridge walks the instance once. */
|
|
90
|
+
_applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
|
|
91
|
+
|
|
92
|
+
/** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
|
|
93
|
+
* a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
|
|
94
|
+
* triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
|
|
95
|
+
* or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
|
|
96
|
+
* only. null = miss, or a host without the pick (desktop today). */
|
|
97
|
+
pickTriangle(screenX: number, screenY: number): TrianglePick | null {
|
|
98
|
+
const json = _creator.pickTriangle?.(this.id, screenX, screenY)
|
|
99
|
+
return json ? JSON.parse(json) as TrianglePick : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
|
|
103
|
+
* the same parent and scene and sharing this model's current transform. The clone has its own
|
|
104
|
+
* independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
|
|
105
|
+
clone(): Model {
|
|
106
|
+
const id = _creator.cloneEntity(this.id)
|
|
107
|
+
const m = new Model(id)
|
|
108
|
+
m._lightmapped = this._lightmapped
|
|
109
|
+
m._foliage = this._foliage
|
|
110
|
+
if (!this._culling) { m._culling = false; _creator.setGlbCulling(id, false) } // the engine default is on
|
|
111
|
+
if (this._lodMesh >= 0) { m._lodMesh = this._lodMesh; _pushLod(m) }
|
|
112
|
+
// the clone is a FRESH gltfio instance — it comes back with the asset's own shadow flags,
|
|
113
|
+
// not this model's, so a cleared flag has to be re-applied
|
|
114
|
+
m._castShadows = this._castShadows
|
|
115
|
+
m._receiveShadows = this._receiveShadows
|
|
116
|
+
if (!this._castShadows || !this._receiveShadows) m._applyShadows()
|
|
117
|
+
return m
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
|
|
121
|
+
static load(
|
|
122
|
+
source: string | FetchResponse,
|
|
123
|
+
options: {
|
|
124
|
+
/** Frustum culling — see `culling` (default: on where the host refits skinned bounds, else off). */
|
|
125
|
+
culling?: boolean
|
|
126
|
+
onProgress?: (p: { loaded: number, total?: number }) => void
|
|
127
|
+
/** Baked lighting (packages/creator-bake). `true` = a STATIC: loads through the lightmap material so
|
|
128
|
+
* `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
|
|
129
|
+
* --lightmap-uv`) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
|
|
130
|
+
* the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
|
|
131
|
+
lightmap?: boolean
|
|
132
|
+
/** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
|
|
133
|
+
* per-copy tint. Hosts without the tier fall back to the standard shader. */
|
|
134
|
+
foliage?: boolean
|
|
135
|
+
} = {},
|
|
136
|
+
): Promise<Model> {
|
|
137
|
+
if (typeof source === "string") {
|
|
138
|
+
return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
|
|
139
|
+
if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
|
|
140
|
+
return Model.load(resp, options)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
return new Promise<Model>((resolve, reject) => {
|
|
144
|
+
// the provider path: a foliage model takes the vegetation tier's pair when the host ships it (fetchLocal
|
|
145
|
+
// id 0 = it does not), a static the lightmap material + its masked twin; anything else the ubershader
|
|
146
|
+
const foliageWanted = !!options.foliage && !!_creator.setNextGlbLightmapped
|
|
147
|
+
let foliage = false
|
|
148
|
+
if (foliageWanted) {
|
|
149
|
+
const fol = Material.idOf(Material._foliageTemplate())
|
|
150
|
+
const folMasked = fol ? Material.idOf(Material._foliageMaskedTemplate()) : 0
|
|
151
|
+
foliage = fol > 0 && folMasked > 0
|
|
152
|
+
if (foliage) _creator.setNextGlbLightmapped!(fol, folMasked)
|
|
153
|
+
}
|
|
154
|
+
const lightmapped = !foliage && !!options.lightmap && !!_creator.setNextGlbLightmapped
|
|
155
|
+
if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
|
|
156
|
+
_creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
|
|
157
|
+
if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
|
|
158
|
+
const model = new Model(entityId)
|
|
159
|
+
model._lightmapped = lightmapped
|
|
160
|
+
model._foliage = foliage
|
|
161
|
+
const culling = options.culling ?? Model._cullingDefault()
|
|
162
|
+
if (!culling) { model._culling = false; _creator.setGlbCulling(entityId, false) }
|
|
163
|
+
resolve(model)
|
|
164
|
+
}, reject)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/gl/Node.ts
CHANGED
|
@@ -12,10 +12,23 @@ import { Quat, cw, type QuatLike } from "../math/quat"
|
|
|
12
12
|
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
13
13
|
import type { Geometry } from "./Geometry"
|
|
14
14
|
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
15
|
-
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
15
|
+
import { ensurePhysicsEvents } from "./physics/physicsEvents"
|
|
16
16
|
import { Material } from "./Material"
|
|
17
17
|
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
18
18
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
19
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
20
|
+
import { CLOCK_GAME, DOM_GL_NODE, eulerValue, quatValue, vec3Value, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
21
|
+
import { tweenBag } from "../animate/tween/Timeline"
|
|
22
|
+
|
|
23
|
+
/** Animatable transform props of a 3D node: a value tweens from the current one, an ARRAY OF
|
|
24
|
+
* VALUES is a keyframe list (`position: [[0,0,0], [0,2,0]]`). */
|
|
25
|
+
export type NodeTweenProps = {
|
|
26
|
+
position?: Vec3Like | Vec3Like[],
|
|
27
|
+
scale?: number | Vec3Like | (number | Vec3Like)[],
|
|
28
|
+
quaternion?: QuatLike | QuatLike[],
|
|
29
|
+
/** Degrees, YXZ; interpolated per axis without shortest-arc, so `[0, 720, 0]` spins twice. */
|
|
30
|
+
eulerAngles?: Vec3Like | Vec3Like[],
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
|
|
21
34
|
export const nodeRegistry = new Registry<Node>()
|
|
@@ -197,6 +210,32 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
|
|
|
197
210
|
if (this._xf) this._setOwnedRotation()
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
// --- keyframe animation (docs/timeline-plan.md) ---
|
|
214
|
+
/** Tween the transform to the given values — `node.animateTo({ position: [0, 2, 0], duration: 800,
|
|
215
|
+
* easing: 'inOutCubic' })`; arrays of values are keyframes. Runs on the game clock (pauses with
|
|
216
|
+
* the game) unless `clock: 'ui'`. Returns the {@link Animation} handle. Phase 1: plain nodes —
|
|
217
|
+
* a physics-owned node (a body / character) is not routed through its engine object yet. */
|
|
218
|
+
animateTo(props: NodeTweenProps & TweenMeta): Animation {
|
|
219
|
+
return tweenBag(this, props as Record<string, unknown>, true)
|
|
220
|
+
}
|
|
221
|
+
/** Tween FROM the given values to the node's current transform (an entrance). */
|
|
222
|
+
animateFrom(props: NodeTweenProps & TweenMeta): Animation {
|
|
223
|
+
return tweenBag(this, props as Record<string, unknown>, false)
|
|
224
|
+
}
|
|
225
|
+
/** @internal keyframe-core target protocol */
|
|
226
|
+
readonly _tweenClock: number = CLOCK_GAME
|
|
227
|
+
/** @internal */
|
|
228
|
+
_tweenChannel(prop: string): TweenChannel | null {
|
|
229
|
+
const id = () => this.id
|
|
230
|
+
switch (prop) {
|
|
231
|
+
case "position": return { domain: DOM_GL_NODE, id, value: vec3Value }
|
|
232
|
+
case "scale": return { domain: DOM_GL_NODE, id, value: vec3Value }
|
|
233
|
+
case "quaternion": return { domain: DOM_GL_NODE, id, value: quatValue }
|
|
234
|
+
case "eulerAngles": return { domain: DOM_GL_NODE, id, value: eulerValue }
|
|
235
|
+
default: return null
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
200
239
|
get eulerAngles(): Vec3 { return new Mat4(this._sync()).eulerAngles }
|
|
201
240
|
// order 1 = YXZ — the SDK's euler convention (math/quat.ts); the getter also extracts YXZ, so
|
|
202
241
|
// the pair round-trips. (Historically this passed 0/XYZ AND the engine stored the euler matrix
|
package/src/gl/Particles.ts
CHANGED
|
@@ -40,6 +40,10 @@ const TAG_ANGULAR_VELOCITY = 17
|
|
|
40
40
|
const TAG_GROUND = 18
|
|
41
41
|
const TAG_VELOCITY_LIFE = 19
|
|
42
42
|
const TAG_ORDER = 20
|
|
43
|
+
const TAG_SMOOTH_PATH = 21
|
|
44
|
+
const TAG_RIBBON_ORIENT = 22
|
|
45
|
+
/** `TrailOptions.orient` → the native axis code (0 = the camera-facing default). */
|
|
46
|
+
const RIBBON_AXIS: Record<"camera" | "x" | "y" | "z", number> = { camera: 0, x: 1, y: 2, z: 3 }
|
|
43
47
|
|
|
44
48
|
/** @internal — exported for the tag-sync test only. */
|
|
45
49
|
export const _particleTags = {
|
|
@@ -49,7 +53,7 @@ export const _particleTags = {
|
|
|
49
53
|
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
50
54
|
RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
|
|
51
55
|
ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
|
|
52
|
-
ORDER: TAG_ORDER,
|
|
56
|
+
ORDER: TAG_ORDER, SMOOTH_PATH: TAG_SMOOTH_PATH, RIBBON_ORIENT: TAG_RIBBON_ORIENT,
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// --- curve builders -----------------------------------------------------------------------------
|
|
@@ -231,7 +235,15 @@ type Shape =
|
|
|
231
235
|
* birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
|
|
232
236
|
* space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
|
|
233
237
|
export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
|
|
234
|
-
type Noise = {
|
|
238
|
+
type Noise = {
|
|
239
|
+
strength?: number, frequency?: number, speed?: number,
|
|
240
|
+
/** How much of the displacement a particle earns with AGE (default 0 = all of it from birth).
|
|
241
|
+
* The field is sampled by position, so at 0 every particle born at the same spot is pushed the
|
|
242
|
+
* same way and a plume's root wanders off its emitter by up to `strength` metres; at 1 a particle
|
|
243
|
+
* is born exactly where the emitter put it and drifts into the field over its life — what you
|
|
244
|
+
* want whenever the source is visible (an exhaust pipe, a contact patch, a muzzle). */
|
|
245
|
+
ramp?: number,
|
|
246
|
+
}
|
|
235
247
|
|
|
236
248
|
/** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
|
|
237
249
|
* world y with `space: 'world'`, emitter-local y otherwise. */
|
|
@@ -278,11 +290,25 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
|
278
290
|
* spread evenly along the path — trail density independent of speed, no per-frame clumps.
|
|
279
291
|
* Meant for `space: 'world'`. */
|
|
280
292
|
rateOverDistance?: number
|
|
293
|
+
/** Spread those spawn points along a CURVE through the emitter's recent path instead of the
|
|
294
|
+
* straight line between where it was last frame and where it is now. A straight line cuts the
|
|
295
|
+
* corner by however far the path bows inside one frame, which grows with the frame TIME — so a
|
|
296
|
+
* fast curving emitter looks faceted, and looks worse the slower the machine. Default false
|
|
297
|
+
* (the straight line); costs three stored positions and one curve evaluation per spawn.
|
|
298
|
+
*
|
|
299
|
+
* The curve needs a point one frame AHEAD, so each frame's spawns are placed provisionally and
|
|
300
|
+
* nudged into place on the next frame, once it exists. Nothing else observes them in between. */
|
|
301
|
+
smooth?: boolean
|
|
281
302
|
/** Draw order among blended systems at the same depth — higher draws later, i.e. on top (Unity's
|
|
282
303
|
* sortingOrder). Systems are depth-sorted by their node, so the emitters of one effect tie and
|
|
283
304
|
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
284
305
|
* Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
|
|
285
306
|
order?: number
|
|
307
|
+
/** Coarse draw order among ALL blended draws, 0 (first) … 7 (last) — see `Mesh.renderPriority`.
|
|
308
|
+
* Default 5: meshes sit at 4 and decals at 3, so smoke covers a car's glass and its skid marks
|
|
309
|
+
* whatever the camera does (the engine's depth sort compares object centres, and an emitter's
|
|
310
|
+
* centre says nothing about where its cloud is). `order` breaks ties inside one priority. */
|
|
311
|
+
renderPriority?: number
|
|
286
312
|
shape?: Shape
|
|
287
313
|
/** Extra velocity over life (radial burst that decays, axis drift) — see `VelocityOverLife`. */
|
|
288
314
|
velocityOverLife?: VelocityOverLife
|
|
@@ -437,6 +463,7 @@ export class Particles extends Node {
|
|
|
437
463
|
super()
|
|
438
464
|
this._material = options.material ?? (options.mesh ? Material.lit() : Material.particles(options))
|
|
439
465
|
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxParticles ?? 0)
|
|
466
|
+
this.renderPriority = options.renderPriority ?? 5
|
|
440
467
|
|
|
441
468
|
const id = this.id
|
|
442
469
|
this.custom = new Proxy([] as ParticleValue[], {
|
|
@@ -463,6 +490,7 @@ export class Particles extends Node {
|
|
|
463
490
|
if (options.space !== undefined) out.push(TAG_SPACE, 1, options.space === "world" ? 1 : 0)
|
|
464
491
|
if (options.inheritVelocity !== undefined) out.push(TAG_INHERIT_VELOCITY, 1, options.inheritVelocity)
|
|
465
492
|
if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
|
|
493
|
+
if (options.smooth !== undefined) out.push(TAG_SMOOTH_PATH, 1, options.smooth ? 1 : 0)
|
|
466
494
|
if (options.order !== undefined) out.push(TAG_ORDER, 1, options.order)
|
|
467
495
|
if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
|
|
468
496
|
if (options.shape !== undefined) pushShape(out, options.shape)
|
|
@@ -496,7 +524,7 @@ export class Particles extends Node {
|
|
|
496
524
|
}
|
|
497
525
|
if (options.noise !== undefined) {
|
|
498
526
|
const n = options.noise
|
|
499
|
-
out.push(TAG_NOISE,
|
|
527
|
+
out.push(TAG_NOISE, 4, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0, n ? n.ramp ?? 0 : 0)
|
|
500
528
|
}
|
|
501
529
|
if (options.seed !== undefined) out.push(TAG_SEED, 1, options.seed)
|
|
502
530
|
|
|
@@ -516,7 +544,13 @@ export class Particles extends Node {
|
|
|
516
544
|
set space(val: "local" | "world") { this._send([ TAG_SPACE, 1, val === "world" ? 1 : 0 ]) }
|
|
517
545
|
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
518
546
|
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
547
|
+
/** Lay a frame's spawns along a curve through the emitter's path, not the straight chord. */
|
|
548
|
+
set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
|
|
519
549
|
set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
|
|
550
|
+
/** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
|
|
551
|
+
set renderPriority(v: number) {
|
|
552
|
+
if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
|
|
553
|
+
}
|
|
520
554
|
|
|
521
555
|
set velocityOverLife(v: VelocityOverLife) {
|
|
522
556
|
const out: number[] = []
|
|
@@ -585,7 +619,7 @@ export class Particles extends Node {
|
|
|
585
619
|
|
|
586
620
|
set noise(noise: Noise | null) {
|
|
587
621
|
this._send(noise
|
|
588
|
-
? [ TAG_NOISE,
|
|
622
|
+
? [ TAG_NOISE, 4, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1, noise.ramp ?? 0 ]
|
|
589
623
|
: [ TAG_NOISE, 3, 0, 0, 0 ])
|
|
590
624
|
}
|
|
591
625
|
}
|
|
@@ -602,8 +636,28 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
|
|
|
602
636
|
width?: ParticleValue
|
|
603
637
|
/** Minimum emitter movement (world units) between recorded points. Default 0.05. */
|
|
604
638
|
minDistance?: number
|
|
605
|
-
/** Point capacity (default 128).
|
|
639
|
+
/** Point capacity (default 128). A spawn that does not fit is DROPPED, so a fast emitter at a
|
|
640
|
+
* small `minDistance` wants headroom: a sword tip covers metres inside one `time` window. */
|
|
606
641
|
maxPoints?: number
|
|
642
|
+
/** Follow a CURVE through the emitter's recent path instead of the straight line between frames.
|
|
643
|
+
* A trail is where this shows most — the strip is the path — and it shows worse the lower the
|
|
644
|
+
* frame rate, since a longer frame bows further off its own chord. Default false; see
|
|
645
|
+
* `ParticlesOptions.smooth`. */
|
|
646
|
+
smooth?: boolean
|
|
647
|
+
/** Which way the strip's WIDTH points.
|
|
648
|
+
*
|
|
649
|
+
* `'camera'` (default) rolls the strip about its own length to stay flat to the viewer — it can
|
|
650
|
+
* never disappear, which is why it is the default, but it bears no relation to the thing that drew
|
|
651
|
+
* it. An AXIS (`'x'`/`'y'`/`'z'`) uses that axis of the emitter's own transform, as it was when
|
|
652
|
+
* each point was laid: on a node riding a sword's blade, `'z'` (the blade) makes the strip the
|
|
653
|
+
* surface the blade actually swept, and `width` stops being a made-up number — it is the blade. */
|
|
654
|
+
orient?: "camera" | "x" | "y" | "z"
|
|
655
|
+
/** With `orient` on an axis: the floor under the strip's APPARENT width, 0..1 of its real width,
|
|
656
|
+
* below which it rolls back toward the camera. An honestly oriented strip goes edge-on — and
|
|
657
|
+
* vanishes — whenever the swing happens in the plane of view; this is the hybrid that keeps it
|
|
658
|
+
* readable. 0 = never roll (honest, and it will vanish), 1 = always (the same as `'camera'`).
|
|
659
|
+
* Default 0.4: a sweep still reads as a sweep, and a swing toward the camera still shows. */
|
|
660
|
+
faceCamera?: number
|
|
607
661
|
color?: ParticleColor
|
|
608
662
|
/** Opacity along the trail. Defaults to fading the tail out (`curve().to(0)`). */
|
|
609
663
|
opacity?: ParticleValue
|
|
@@ -617,6 +671,8 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
|
|
|
617
671
|
export class Trail extends Node {
|
|
618
672
|
private _material: Material
|
|
619
673
|
private _rateDistance: number
|
|
674
|
+
private _orient = 0
|
|
675
|
+
private _faceCamera = 0.4
|
|
620
676
|
|
|
621
677
|
constructor(options: TrailOptions = {}) {
|
|
622
678
|
super()
|
|
@@ -632,6 +688,12 @@ export class Trail extends Node {
|
|
|
632
688
|
TAG_RATE_DISTANCE, 1, this._rateDistance,
|
|
633
689
|
TAG_LIFETIME, 2, time, time,
|
|
634
690
|
]
|
|
691
|
+
if (options.smooth) out.push(TAG_SMOOTH_PATH, 1, 1)
|
|
692
|
+
if (options.orient !== undefined || options.faceCamera !== undefined) {
|
|
693
|
+
this._orient = RIBBON_AXIS[options.orient ?? "camera"]
|
|
694
|
+
if (options.faceCamera !== undefined) this._faceCamera = options.faceCamera
|
|
695
|
+
out.push(TAG_RIBBON_ORIENT, 2, this._orient, this._faceCamera)
|
|
696
|
+
}
|
|
635
697
|
pushParam(out, 0, options.width ?? 0.1)
|
|
636
698
|
pushParam(out, 2, options.opacity ?? curve().to(0))
|
|
637
699
|
if (options.color !== undefined) pushColor(out, options.color)
|
|
@@ -668,6 +730,21 @@ export class Trail extends Node {
|
|
|
668
730
|
this._send([ TAG_RATE_DISTANCE, 1, this._rateDistance ])
|
|
669
731
|
}
|
|
670
732
|
|
|
733
|
+
/** Follow a curve through the emitter's path instead of the straight chord between frames. */
|
|
734
|
+
set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
|
|
735
|
+
|
|
736
|
+
/** Which way the strip's width points — see `TrailOptions.orient`. Keeps the current `faceCamera`. */
|
|
737
|
+
set orient(val: "camera" | "x" | "y" | "z") {
|
|
738
|
+
this._orient = RIBBON_AXIS[val]
|
|
739
|
+
this._send([ TAG_RIBBON_ORIENT, 2, this._orient, this._faceCamera ])
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The hybrid's floor — see `TrailOptions.faceCamera`. */
|
|
743
|
+
set faceCamera(val: number) {
|
|
744
|
+
this._faceCamera = val
|
|
745
|
+
this._send([ TAG_RIBBON_ORIENT, 2, this._orient, val ])
|
|
746
|
+
}
|
|
747
|
+
|
|
671
748
|
/** Pause/resume laying down points — existing ones still age out, so the trail fades naturally
|
|
672
749
|
* after e.g. a sword swing ends. */
|
|
673
750
|
set emitting(val: boolean) { this._send([ TAG_RATE_DISTANCE, 1, val ? this._rateDistance : 0 ]) }
|
package/src/gl/Scene.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
|
13
13
|
import type { FetchResponse } from "../runtime/fetch"
|
|
14
14
|
import { _requestCameraPermission } from "../plugins/permission"
|
|
15
15
|
import { Camera } from "./Camera"
|
|
16
|
-
import { SceneAudio } from "./SceneAudio"
|
|
16
|
+
import { SceneAudio } from "./audio/SceneAudio"
|
|
17
17
|
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
|
|
18
18
|
import { Material } from "./Material"
|
|
19
19
|
import { Texture } from "./Texture"
|
|
@@ -177,7 +177,8 @@ const installRenderSyncedFrames = (): void => {
|
|
|
177
177
|
export class Scene implements Presentable {
|
|
178
178
|
/** @internal native scene handle. */
|
|
179
179
|
readonly _id: number
|
|
180
|
-
/** @internal
|
|
180
|
+
/** @internal what `environmentIntensity` currently asks for — the accessor's backing field, so it
|
|
181
|
+
* answers on hosts whose engine has no live setter too. */
|
|
181
182
|
_environmentIntensity = 20000
|
|
182
183
|
readonly camera: Camera
|
|
183
184
|
/** The listener + global 3D audio knobs (docs/audio-plan.md). */
|
|
@@ -309,6 +310,26 @@ export class Scene implements Presentable {
|
|
|
309
310
|
setBloom(enabled: boolean, intensity = 0.2): void {
|
|
310
311
|
_creator.setBloomOptions(this._id, enabled, intensity, 1)
|
|
311
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* How bright the environment (IBL) lights the scene, in lux — `SceneOptions.environmentIntensity`
|
|
315
|
+
* after the fact. Live: it changes the probe's intensity, not the probe, so it costs nothing and
|
|
316
|
+
* can be dragged. (`setDefaultIbl`, the call that installs a probe, rebuilds the cubemap from the
|
|
317
|
+
* ktx every time — never drive a slider through a scene option that has to re-open.)
|
|
318
|
+
*
|
|
319
|
+
* `lecodes lightmap bake` reads the same number off the live probe, so a scene dimmed here bakes
|
|
320
|
+
* dimmed. Hosts without the call keep whatever the scene opened with, and the getter still
|
|
321
|
+
* reports what was asked for.
|
|
322
|
+
*/
|
|
323
|
+
get environmentIntensity(): number { return this._environmentIntensity }
|
|
324
|
+
set environmentIntensity(lux: number) {
|
|
325
|
+
this._environmentIntensity = lux
|
|
326
|
+
if (_creator.setEnvironmentIntensity) _creator.setEnvironmentIntensity(this._id, lux)
|
|
327
|
+
else if (!Scene._warnedEnvIntensity) {
|
|
328
|
+
Scene._warnedEnvIntensity = true
|
|
329
|
+
console.warn("[scene] this host has no setEnvironmentIntensity — environmentIntensity stays as the scene opened")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
private static _warnedEnvIntensity = false
|
|
312
333
|
/** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
|
|
313
334
|
setStencil(enabled: boolean): void {
|
|
314
335
|
if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
|
|
@@ -367,6 +388,18 @@ export class Scene implements Presentable {
|
|
|
367
388
|
return new Promise<void>((res) => _creator.warmRender(this._id, res))
|
|
368
389
|
}
|
|
369
390
|
|
|
391
|
+
/** Precompile the scene's shader variants so nothing is compiled mid-game. Every material the host
|
|
392
|
+
* knows is queued for the sun / shadow / fog / skinning variants and the dynamic-light key — the
|
|
393
|
+
* first point light (a muzzle flash, an explosion) would otherwise rebuild every lit shader in view
|
|
394
|
+
* on that frame. Call it once the sun and fog are set, typically under a loading screen; materials
|
|
395
|
+
* loaded later are queued in the background as they are created. Resolves at once on a host
|
|
396
|
+
* without the bridge. */
|
|
397
|
+
precompileShaders(): Promise<void> {
|
|
398
|
+
const fn = _creator.precompileShaders
|
|
399
|
+
if (!fn) return Promise.resolve()
|
|
400
|
+
return new Promise<void>((res) => fn.call(_creator, this._id, res))
|
|
401
|
+
}
|
|
402
|
+
|
|
370
403
|
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
371
404
|
// A scene is a destination like a screen: opening it replaces whatever is visible (suspending
|
|
372
405
|
// an active Router until Router.restore()), and it can be pushed onto the Router stack. The
|