lecodes-cli 0.17.0 → 0.17.1
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 +68 -10
- package/package.json +2 -2
- package/runtime/sdk/gl/IK.ts +174 -170
- package/runtime/sdk/gl/InstancedMesh.ts +132 -0
- package/runtime/sdk/gl/Node.ts +285 -272
- package/runtime/sdk/gl/Particles.ts +625 -546
- package/runtime/sdk/inject.ts +162 -160
- package/runtime/sdk-types.json +1 -1
package/runtime/sdk/gl/Node.ts
CHANGED
|
@@ -1,272 +1,285 @@
|
|
|
1
|
-
// Base 3D scene node — an empty transform. Mesh and Light extend it, so every node operation works
|
|
2
|
-
// on them. Native (creator-gl) owns the authoritative transform; reads decompose a cached local
|
|
3
|
-
// matrix (re-synced lazily, since physics can write the transform from native each frame).
|
|
4
|
-
//
|
|
5
|
-
// Capabilities are aspects: node.aspect(Physics, …) (→ node.physics), node.aspect(Shape, …).
|
|
6
|
-
// A GLB import is the Model node kind (with the ModelAnimation aspect). Hierarchy lives here.
|
|
7
|
-
|
|
8
|
-
import { AspectHost } from "../core/Aspect"
|
|
9
|
-
import { Registry } from "../core/registry"
|
|
10
|
-
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
11
|
-
import { Quat, cw, type QuatLike } from "../math/quat"
|
|
12
|
-
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
13
|
-
import type { Geometry } from "./Geometry"
|
|
14
|
-
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
15
|
-
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
16
|
-
import { glState } from "./state"
|
|
17
|
-
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
18
|
-
|
|
19
|
-
/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
|
|
20
|
-
export const nodeRegistry = new Registry<Node>()
|
|
21
|
-
|
|
22
|
-
export type NodeEvents = {
|
|
23
|
-
click: (ev: ClickEvent<Node | null>) => void
|
|
24
|
-
touchstart: (ev: TouchStartEvent<Node | null>) => void
|
|
25
|
-
/** A looping GLB animation clip wrapped around. */
|
|
26
|
-
loopReached: (clip: number) => void
|
|
27
|
-
/** A non-looping GLB animation clip finished. */
|
|
28
|
-
completed: (clip: number) => void
|
|
29
|
-
/** AR anchors only (`scene.root` / `scene.createAnchor()`): the anchor began tracking. */
|
|
30
|
-
track: () => void
|
|
31
|
-
/** AR anchors only: the anchor lost tracking. */
|
|
32
|
-
untrack: () => void
|
|
33
|
-
/** A physics contact/trigger overlap began (the other body's node). Needs a Shape + Physics/Trigger. */
|
|
34
|
-
enter: (other: Node) => void
|
|
35
|
-
/** A physics contact/trigger overlap ended. */
|
|
36
|
-
exit: (other: Node) => void
|
|
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
|
-
|
|
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
|
-
get
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
get
|
|
136
|
-
set
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
_creator.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
get
|
|
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
|
-
_creator.
|
|
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
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
const
|
|
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
|
-
this
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1
|
+
// Base 3D scene node — an empty transform. Mesh and Light extend it, so every node operation works
|
|
2
|
+
// on them. Native (creator-gl) owns the authoritative transform; reads decompose a cached local
|
|
3
|
+
// matrix (re-synced lazily, since physics can write the transform from native each frame).
|
|
4
|
+
//
|
|
5
|
+
// Capabilities are aspects: node.aspect(Physics, …) (→ node.physics), node.aspect(Shape, …).
|
|
6
|
+
// A GLB import is the Model node kind (with the ModelAnimation aspect). Hierarchy lives here.
|
|
7
|
+
|
|
8
|
+
import { AspectHost } from "../core/Aspect"
|
|
9
|
+
import { Registry } from "../core/registry"
|
|
10
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
11
|
+
import { Quat, cw, type QuatLike } from "../math/quat"
|
|
12
|
+
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
13
|
+
import type { Geometry } from "./Geometry"
|
|
14
|
+
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
15
|
+
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
16
|
+
import { glState } from "./state"
|
|
17
|
+
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
18
|
+
|
|
19
|
+
/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
|
|
20
|
+
export const nodeRegistry = new Registry<Node>()
|
|
21
|
+
|
|
22
|
+
export type NodeEvents = {
|
|
23
|
+
click: (ev: ClickEvent<Node | null>) => void
|
|
24
|
+
touchstart: (ev: TouchStartEvent<Node | null>) => void
|
|
25
|
+
/** A looping GLB animation clip wrapped around. */
|
|
26
|
+
loopReached: (clip: number) => void
|
|
27
|
+
/** A non-looping GLB animation clip finished. */
|
|
28
|
+
completed: (clip: number) => void
|
|
29
|
+
/** AR anchors only (`scene.root` / `scene.createAnchor()`): the anchor began tracking. */
|
|
30
|
+
track: () => void
|
|
31
|
+
/** AR anchors only: the anchor lost tracking. */
|
|
32
|
+
untrack: () => void
|
|
33
|
+
/** A physics contact/trigger overlap began (the other body's node). Needs a Shape + Physics/Trigger. */
|
|
34
|
+
enter: (other: Node) => void
|
|
35
|
+
/** A physics contact/trigger overlap ended. */
|
|
36
|
+
exit: (other: Node) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const shortName = (s: string): string => s.slice(s.search(/[^:|]*$/))
|
|
40
|
+
|
|
41
|
+
/** Fallback for hosts without `_creator.findNode` (exact name, then short name, first in tree order —
|
|
42
|
+
* no skin-joint preference: that needs the host's skin knowledge). 0 when absent. */
|
|
43
|
+
const findNodeByWalk = (rootId: number, name: string): number => {
|
|
44
|
+
let exact = 0
|
|
45
|
+
let loose = 0
|
|
46
|
+
const want = shortName(name)
|
|
47
|
+
_creator.traverse(rootId, (id) => {
|
|
48
|
+
if (exact) return
|
|
49
|
+
const n = _creator.getName(id)
|
|
50
|
+
if (n === name) exact = id
|
|
51
|
+
else if (!loose && shortName(n) === want) loose = id
|
|
52
|
+
})
|
|
53
|
+
return exact || loose
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Node extends AspectHost<NodeEvents> {
|
|
57
|
+
/** Native entity handle. */
|
|
58
|
+
readonly id: number
|
|
59
|
+
|
|
60
|
+
/** AR anchors only — whether the anchor is currently being tracked. Kept in step with the
|
|
61
|
+
* 'track' / 'untrack' events; `false` for any non-anchor node. */
|
|
62
|
+
isTracked = false
|
|
63
|
+
|
|
64
|
+
private _matrix?: Float32Array
|
|
65
|
+
private _lastSync = 0
|
|
66
|
+
private _syncFrame = -1
|
|
67
|
+
private _worldMatrix?: Float32Array
|
|
68
|
+
private _scaleCache?: [number, number, number]
|
|
69
|
+
private _boneCache?: Map<string, Node | null>
|
|
70
|
+
|
|
71
|
+
/** @internal — the active animation aspect's clip-complete handler (installed by ModelAnimation). */
|
|
72
|
+
_animComplete?: (clipIndex: number) => boolean
|
|
73
|
+
|
|
74
|
+
constructor(internalId?: number) {
|
|
75
|
+
super()
|
|
76
|
+
this.id = internalId ?? _creator.createEntity()
|
|
77
|
+
nodeRegistry.set(this.id, this)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Geometry, if this node is a Mesh (used by physics auto-shape). Overridden by Mesh. */
|
|
81
|
+
get geometry(): Geometry | undefined { return undefined }
|
|
82
|
+
|
|
83
|
+
// --- name / visibility ---
|
|
84
|
+
get name(): string { return _creator.getName(this.id) }
|
|
85
|
+
set name(value: string) { _creator.setName(this.id, value) }
|
|
86
|
+
get visible(): boolean { return _creator.isVisible(this.id) }
|
|
87
|
+
set visible(value: boolean) { _creator.setVisible(this.id, value) }
|
|
88
|
+
|
|
89
|
+
// --- local transform ---
|
|
90
|
+
// Native owns the authoritative matrix; we keep a Float32Array cache (re-synced lazily because physics
|
|
91
|
+
// can rewrite it each frame). The PUBLIC `matrix` getter hands back a fresh Mat4 *copy* (value
|
|
92
|
+
// semantics — mutating it never touches the node until you assign it back). `_sync()` is the internal
|
|
93
|
+
// hot path that returns the cache directly for cheap component reads.
|
|
94
|
+
private _sync(): Float32Array {
|
|
95
|
+
if (!this._matrix) this._matrix = new Float32Array(16)
|
|
96
|
+
// stale once per render frame (glState.frame — physics/controllers/animation rewrite transforms
|
|
97
|
+
// between frames), or after 10 ms of wall time when no scene loop is running
|
|
98
|
+
if (this._lastSync === 0 || this._syncFrame !== glState.frame || Date.now() - this._lastSync > 10) {
|
|
99
|
+
_creator.getMatrix(this.id, this._matrix)
|
|
100
|
+
this._scaleCache = undefined
|
|
101
|
+
this._lastSync = Date.now()
|
|
102
|
+
this._syncFrame = glState.frame
|
|
103
|
+
}
|
|
104
|
+
return this._matrix
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get matrix(): Mat4 { return new Mat4(this._sync()) }
|
|
108
|
+
set matrix(m: Mat4Like) {
|
|
109
|
+
if (!this._matrix) this._matrix = new Float32Array(16)
|
|
110
|
+
this._matrix.set(matArr(m))
|
|
111
|
+
this._lastSync = Date.now()
|
|
112
|
+
_creator.setMatrix(this.id, this._matrix)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get worldMatrix(): Mat4 {
|
|
116
|
+
if (this.parent === null) return this.matrix
|
|
117
|
+
if (!this._worldMatrix) this._worldMatrix = new Float32Array(16)
|
|
118
|
+
_creator.getWorldMatrix(this.id, this._worldMatrix)
|
|
119
|
+
return new Mat4(this._worldMatrix)
|
|
120
|
+
}
|
|
121
|
+
set worldMatrix(m: Mat4Like) {
|
|
122
|
+
const p = this.parent
|
|
123
|
+
if (p === null) { this.matrix = m; return }
|
|
124
|
+
// local = parentWorld⁻¹ · world, composed here: the native setWorldMatrix ignores the parent (it
|
|
125
|
+
// wrote the world matrix as the local one — a bone under a rotated Model wrapper read back with the
|
|
126
|
+
// ancestors' rotation applied twice)
|
|
127
|
+
this.matrix = p.worldMatrix.invert().mul(m)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get position(): Vec3 { const m = this._sync(); return new Vec3(m[12], m[13], m[14]) }
|
|
131
|
+
set position(v: Vec3Like) { this._lastSync = 0; _creator.setPosition(this.id, cx(v), cy(v), cz(v)) }
|
|
132
|
+
|
|
133
|
+
// Scalar position accessors — the loud, correct way to nudge one axis (`node.x = 3`), so nobody reaches
|
|
134
|
+
// for `node.position.x = 3` (a no-op: the getter returns a fresh copy).
|
|
135
|
+
get x(): number { return this._sync()[12] }
|
|
136
|
+
set x(v: number) { const m = this._sync(); this._lastSync = 0; _creator.setPosition(this.id, v, m[13], m[14]) }
|
|
137
|
+
get y(): number { return this._sync()[13] }
|
|
138
|
+
set y(v: number) { const m = this._sync(); this._lastSync = 0; _creator.setPosition(this.id, m[12], v, m[14]) }
|
|
139
|
+
get z(): number { return this._sync()[14] }
|
|
140
|
+
set z(v: number) { const m = this._sync(); this._lastSync = 0; _creator.setPosition(this.id, m[12], m[13], v) }
|
|
141
|
+
|
|
142
|
+
get scale(): Vec3 {
|
|
143
|
+
if (this._lastSync === 0 || !this._scaleCache) {
|
|
144
|
+
const m = this._sync()
|
|
145
|
+
this._scaleCache = [ Math.hypot(m[0], m[1], m[2]), Math.hypot(m[4], m[5], m[6]), Math.hypot(m[8], m[9], m[10]) ]
|
|
146
|
+
}
|
|
147
|
+
return new Vec3(this._scaleCache)
|
|
148
|
+
}
|
|
149
|
+
set scale(v: Vec3Like | number) {
|
|
150
|
+
this._lastSync = 0
|
|
151
|
+
if (typeof v === "number") _creator.setScale(this.id, v, v, v)
|
|
152
|
+
else _creator.setScale(this.id, cx(v), cy(v), cz(v))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get quaternion(): Quat { return new Mat4(this._sync()).rotation }
|
|
156
|
+
set quaternion(v: QuatLike) { this._lastSync = 0; _creator.setQuaternion(this.id, cx(v), cy(v), cz(v), cw(v)) }
|
|
157
|
+
|
|
158
|
+
get eulerAngles(): Vec3 { return new Mat4(this._sync()).eulerAngles }
|
|
159
|
+
// order 1 = YXZ — the SDK's euler convention (math/quat.ts); the getter also extracts YXZ, so
|
|
160
|
+
// the pair round-trips. (Historically this passed 0/XYZ AND the engine stored the euler matrix
|
|
161
|
+
// transposed — the setter applied the INVERSE rotation. Both fixed 2026-07-10.)
|
|
162
|
+
set eulerAngles(v: Vec3Like) { this._lastSync = 0; _creator.setEulerAngles(this.id, cx(v), cy(v), cz(v), 1) }
|
|
163
|
+
|
|
164
|
+
// --- world-space reads ---
|
|
165
|
+
get forward(): Vec3 {
|
|
166
|
+
const v = new Float32Array(3)
|
|
167
|
+
_creator.getWorldDirection(this.id, 2, v)
|
|
168
|
+
return new Vec3(-v[0], -v[1], -v[2])
|
|
169
|
+
}
|
|
170
|
+
get worldPosition(): Vec3 {
|
|
171
|
+
const v = new Float32Array(3)
|
|
172
|
+
_creator.getWorldPosition(this.id, v)
|
|
173
|
+
return new Vec3(v[0], v[1], v[2])
|
|
174
|
+
}
|
|
175
|
+
get worldScale(): Vec3 { return this.worldMatrix.scaling }
|
|
176
|
+
get worldQuaternion(): Quat { return this.worldMatrix.rotation }
|
|
177
|
+
get worldEulerAngles(): Vec3 { return this.worldMatrix.eulerAngles }
|
|
178
|
+
|
|
179
|
+
// --- hierarchy ---
|
|
180
|
+
get parent(): Node | null {
|
|
181
|
+
const id = _creator.getParent(this.id)
|
|
182
|
+
return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
|
|
183
|
+
}
|
|
184
|
+
get children(): Node[] {
|
|
185
|
+
return _creator.getChildren(this.id).map((id) => nodeRegistry.get(id) ?? new Node(id))
|
|
186
|
+
}
|
|
187
|
+
get childCount(): number { return _creator.getChildCount(this.id) }
|
|
188
|
+
getChild(index: number): Node | null {
|
|
189
|
+
const id = _creator.getChild(this.id, index)
|
|
190
|
+
return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
|
|
191
|
+
}
|
|
192
|
+
/** Parent the given nodes under this one. */
|
|
193
|
+
add(...children: Node[]): this {
|
|
194
|
+
for (const c of children) _creator.setParent(c.id, this.id, false)
|
|
195
|
+
return this
|
|
196
|
+
}
|
|
197
|
+
setParent(parent: Node | null, worldPositionStays = false): this {
|
|
198
|
+
if (parent === null) _creator.setParentNull(this.id, worldPositionStays)
|
|
199
|
+
else _creator.setParent(this.id, parent.id, worldPositionStays)
|
|
200
|
+
if (worldPositionStays) this._lastSync = 0
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
traverse(callback: (node: Node) => void): void {
|
|
204
|
+
callback(this)
|
|
205
|
+
_creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new Node(id)))
|
|
206
|
+
}
|
|
207
|
+
/** Find a descendant by name — bones of a loaded Model included (`hero.bone('RightHand').add(sword)`).
|
|
208
|
+
* Same rule the Animator binds clips with: exact name first, then the part after the last `:` / `|`
|
|
209
|
+
* (Mixamo `mixamorig:Hips` matches `Hips`); a skinned joint beats a plain node of the same name.
|
|
210
|
+
* Cached per name; null if absent. */
|
|
211
|
+
bone(name: string): Node | null {
|
|
212
|
+
const cache = (this._boneCache ??= new Map<string, Node | null>())
|
|
213
|
+
const hit = cache.get(name)
|
|
214
|
+
if (hit !== undefined) return hit
|
|
215
|
+
const id = _creator.findNode ? _creator.findNode(this.id, name) : findNodeByWalk(this.id, name)
|
|
216
|
+
const found = id ? nodeRegistry.get(id) ?? new Node(id) : null
|
|
217
|
+
cache.set(name, found)
|
|
218
|
+
return found
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- behaviors are aspects: node.aspect(Shape, …) / node.aspect(Physics, …) ---
|
|
222
|
+
|
|
223
|
+
/** Orient the node so the given axis (default "-z", "forward") points at a world point. */
|
|
224
|
+
lookAt(point: Vec3Like, mode: "z" | "-z" | "x" | "-x" | "y" | "-y" = "-z", ortho: Vec3Like = [ 0, 1, 0 ]): this {
|
|
225
|
+
const world = this.worldMatrix
|
|
226
|
+
const pos = world.position
|
|
227
|
+
let dir = new Vec3(point).sub(pos)
|
|
228
|
+
const len = dir.length()
|
|
229
|
+
if (len === 0) return this
|
|
230
|
+
let axis: string = mode
|
|
231
|
+
if (mode.startsWith("-")) { dir = dir.scale(-1 / len); axis = mode.slice(1) }
|
|
232
|
+
else dir = dir.scale(1 / len)
|
|
233
|
+
|
|
234
|
+
const o = new Vec3(ortho)
|
|
235
|
+
let forward: Vec3, right: Vec3, up: Vec3
|
|
236
|
+
if (axis === "x") {
|
|
237
|
+
right = dir; up = o.cross(right).normalize(); forward = up.cross(right).normalize()
|
|
238
|
+
} else if (axis === "y") {
|
|
239
|
+
up = dir; right = o.cross(up).normalize(); forward = up.cross(right).normalize()
|
|
240
|
+
} else {
|
|
241
|
+
forward = dir; right = o.cross(dir).normalize(); up = dir.cross(right)
|
|
242
|
+
}
|
|
243
|
+
const s = world.scaling, w = world.m
|
|
244
|
+
this.worldMatrix = Float32Array.of(
|
|
245
|
+
right.x * s.x, right.y * s.x, right.z * s.x, 0,
|
|
246
|
+
up.x * s.y, up.y * s.y, up.z * s.y, 0,
|
|
247
|
+
forward.x * s.z, forward.y * s.z, forward.z * s.z, 0,
|
|
248
|
+
w[12], w[13], w[14], w[15],
|
|
249
|
+
)
|
|
250
|
+
return this
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- events ---
|
|
254
|
+
override addEventListener<K extends keyof NodeEvents>(channel: K, callback: NodeEvents[K]): void {
|
|
255
|
+
if (channel === "click") registerTouchEndEvent()
|
|
256
|
+
else if (channel === "touchstart") registerTouchStartEvent()
|
|
257
|
+
else if (channel === "enter" || channel === "exit") ensurePhysicsEvents()
|
|
258
|
+
super.addEventListener(channel, callback)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** @internal — called by the touch system to deliver a hit. */
|
|
262
|
+
_dispatchEvent<K extends keyof NodeEvents>(channel: K, ...args: Parameters<NodeEvents[K]>): void {
|
|
263
|
+
this.dispatch(channel, ...args)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @internal — emit a physics contact/trigger event (the physics-event router can't reach dispatch). */
|
|
267
|
+
_emitCollision(channel: "enter" | "exit", other: Node): void { this.dispatch(channel, other) }
|
|
268
|
+
|
|
269
|
+
/** @internal — host animation-complete callback; forwards to the ModelAnimation aspect (if any).
|
|
270
|
+
* Returns whether the clip should keep playing. */
|
|
271
|
+
_onAnimationComplete(clipIndex: number): boolean {
|
|
272
|
+
return this._animComplete?.(clipIndex) ?? true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @internal — emit a GLB animation event (the ModelAnimation aspect can't reach protected dispatch). */
|
|
276
|
+
_emitAnim(channel: "loopReached" | "completed", clip: number): void { this.dispatch(channel, clip) }
|
|
277
|
+
|
|
278
|
+
destroy(): void {
|
|
279
|
+
nodeRegistry.delete(this.id)
|
|
280
|
+
_creator.destroyEntity(this.id)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Read the raw column-major elements out of a Mat4 wrapper or a plain length-16 array. */
|
|
285
|
+
const matArr = (m: Mat4Like): ArrayLike<number> => (m as { m?: ArrayLike<number> }).m ?? (m as ArrayLike<number>)
|