lecodes-sdk 1.1.0 → 1.2.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/inject.js CHANGED
@@ -1235,88 +1235,6 @@ var Color = {
1235
1235
  }
1236
1236
  };
1237
1237
 
1238
- // src/core/events.ts
1239
- var Emitter = class {
1240
- _listeners = {};
1241
- addEventListener(channel, callback) {
1242
- const list = this._listeners[channel];
1243
- if (list) list.push(callback);
1244
- else this._listeners[channel] = [callback];
1245
- }
1246
- removeEventListener(channel, callback) {
1247
- const list = this._listeners[channel];
1248
- if (!list) return;
1249
- const i = list.indexOf(callback);
1250
- if (i >= 0) list.splice(i, 1);
1251
- }
1252
- /** Fire all listeners on a channel. Protected: only the owning class emits. */
1253
- dispatch(channel, ...args) {
1254
- const list = this._listeners[channel];
1255
- if (!list) return;
1256
- for (const fn of list.slice()) fn(...args);
1257
- }
1258
- hasListeners(channel) {
1259
- const list = this._listeners[channel];
1260
- return !!list && list.length > 0;
1261
- }
1262
- /** Drop every listener on every channel (used when the owning object is disposed). */
1263
- clearListeners() {
1264
- this._listeners = {};
1265
- }
1266
- };
1267
-
1268
- // src/core/Aspect.ts
1269
- var AspectHost = class extends Emitter {
1270
- /** @internal class → instance, the authoritative store (named accessors mirror this). */
1271
- _aspects = /* @__PURE__ */ new Map();
1272
- /** Attach (and configure) an aspect, or reconfigure it if already present. Returns the node typed
1273
- * as now-having that aspect (so no guard/`?.` is needed afterwards). Rejects a wrong target node. */
1274
- aspect(ctor, opts) {
1275
- const host = this;
1276
- let inst = host._aspects.get(ctor);
1277
- const fresh = inst === void 0;
1278
- if (!inst) {
1279
- inst = new ctor();
1280
- inst.node = this;
1281
- host._aspects.set(ctor, inst);
1282
- const name = ctor.aspect;
1283
- if (name) host[name] = inst;
1284
- }
1285
- if (opts) Object.assign(inst, opts);
1286
- if (fresh) inst.onAttach?.();
1287
- return this;
1288
- }
1289
- /** Safe access — undefined if the aspect isn't attached. */
1290
- get(ctor) {
1291
- return this._aspects.get(ctor);
1292
- }
1293
- /** Existence check AND type guard: inside `if (node.has(Physics))`, `node.physics` is present. */
1294
- has(ctor) {
1295
- return this._aspects.has(ctor);
1296
- }
1297
- /** Detach an aspect (runs its onDetach). Named `removeAspect` so it never collides with the
1298
- * child-management `add`/`remove` semantics some node kinds expose. */
1299
- removeAspect(ctor) {
1300
- const inst = this._aspects.get(ctor);
1301
- if (inst) {
1302
- inst.onDetach?.();
1303
- this._aspects.delete(ctor);
1304
- const name = ctor.aspect;
1305
- if (name) delete this[name];
1306
- }
1307
- return this;
1308
- }
1309
- };
1310
- var Aspect = class {
1311
- // NOTE: P has no `extends AspectHost` constraint on purpose — Emitter<E>'s generic
1312
- // addEventListener makes a specific-event node (AspectHost<NodeEvents>) not assignable to
1313
- // AspectHost<any>, which would reject every real node. P only types `this.node`; target
1314
- // enforcement happens in aspect() via `this: Self extends TargetOf<A>`. Defaults to Node
1315
- // (3D is the default engine) — a 2D aspect declares its own P, e.g. Aspect<'x', Sprite>.
1316
- /** The node this aspect is attached to (set at attach time). */
1317
- node;
1318
- };
1319
-
1320
1238
  // src/runtime/misc.ts
1321
1239
  var toast = (msg) => _creatorUI.toast(msg);
1322
1240
  var SvgSource = (svg) => ({ svg, tintColor: null });
@@ -1418,6 +1336,36 @@ var Input = {
1418
1336
  }
1419
1337
  };
1420
1338
 
1339
+ // src/core/events.ts
1340
+ var Emitter = class {
1341
+ _listeners = {};
1342
+ addEventListener(channel, callback) {
1343
+ const list = this._listeners[channel];
1344
+ if (list) list.push(callback);
1345
+ else this._listeners[channel] = [callback];
1346
+ }
1347
+ removeEventListener(channel, callback) {
1348
+ const list = this._listeners[channel];
1349
+ if (!list) return;
1350
+ const i = list.indexOf(callback);
1351
+ if (i >= 0) list.splice(i, 1);
1352
+ }
1353
+ /** Fire all listeners on a channel. Protected: only the owning class emits. */
1354
+ dispatch(channel, ...args) {
1355
+ const list = this._listeners[channel];
1356
+ if (!list) return;
1357
+ for (const fn of list.slice()) fn(...args);
1358
+ }
1359
+ hasListeners(channel) {
1360
+ const list = this._listeners[channel];
1361
+ return !!list && list.length > 0;
1362
+ }
1363
+ /** Drop every listener on every channel (used when the owning object is disposed). */
1364
+ clearListeners() {
1365
+ this._listeners = {};
1366
+ }
1367
+ };
1368
+
1421
1369
  // src/runtime/net.ts
1422
1370
  var WebSocket = class extends Emitter {
1423
1371
  _id = null;
@@ -1637,7 +1585,7 @@ var Registry = class {
1637
1585
 
1638
1586
  // src/g2/Node2D.ts
1639
1587
  var node2dRegistry = new Registry();
1640
- var Node2D = class extends AspectHost {
1588
+ var Node2D = class extends Emitter {
1641
1589
  /** Native entity handle. */
1642
1590
  id;
1643
1591
  _x = 0;
@@ -1717,14 +1665,16 @@ var Node2D = class extends AspectHost {
1717
1665
  this._visible = v;
1718
1666
  _creator2d.setVisible(this.id, v);
1719
1667
  }
1720
- /** @internal — host animation events route here by id; the SpriteAnimation aspect installs an
1721
- * instance override that translates clip ids to names and emits 'loopReached' / 'completed'. */
1722
- _dispatchAnimEvent(_clipId, _type) {
1668
+ /**
1669
+ * Attach a collider for overlap detection. DEFERRED: native 2D physics/triggers are not built yet,
1670
+ * so this records intent and is a no-op until the engine lands — the 'enter'/'exit' events won't
1671
+ * fire. The API is here so game code that uses it compiles and stays stable.
1672
+ */
1673
+ collider(_options = {}) {
1674
+ return this;
1723
1675
  }
1724
- /** @internal — emit a translated animation event (the SpriteAnimation aspect can't reach the
1725
- * protected dispatch directly). */
1726
- _emitAnim(channel, clip) {
1727
- this.dispatch(channel, clip);
1676
+ /** @internal — overridden by Sprite to translate native clip ids into named animation events. */
1677
+ _dispatchAnimEvent(_clipId, _type) {
1728
1678
  }
1729
1679
  destroy() {
1730
1680
  node2dRegistry.delete(this.id);
@@ -1887,6 +1837,9 @@ var Sprite = class extends Node2D {
1887
1837
  _flipX = false;
1888
1838
  _flipY = false;
1889
1839
  _size = null;
1840
+ _clipIds = {};
1841
+ _clipNames = {};
1842
+ _current = null;
1890
1843
  constructor(options = {}) {
1891
1844
  super();
1892
1845
  if (options.texture) this.texture = options.texture;
@@ -1960,92 +1913,69 @@ var Sprite = class extends Node2D {
1960
1913
  this._flipY = v;
1961
1914
  _creator2d.setSpriteFlip(this.id, this._flipX, v);
1962
1915
  }
1963
- };
1964
-
1965
- // src/g2/SpriteAnimation.ts
1966
- var SpriteAnimation = class extends Aspect {
1967
- static aspect = "anim";
1968
- /** Grid cell size in texture pixels (defaults to the full texture). */
1969
- size;
1970
- /** Default fps for clips that don't override it. */
1971
- fps = 12;
1972
- /** Default loop for clips that don't override it. */
1973
- loop = true;
1974
- /** Named clips; frame indices are row-major into the grid. Set via the attach opts. */
1975
- clips = {};
1976
- _clipIds = {};
1977
- _clipNames = {};
1978
- _current = null;
1979
- onAttach() {
1980
- this._define();
1981
- this.node._dispatchAnimEvent = (clipId, type) => this.node._emitAnim(type === 1 ? "completed" : "loopReached", this._clipNames[clipId] ?? "");
1982
- ensureAnimEvents();
1983
- }
1984
- /** (Re)build native clips from `this.clips`. Called on attach; call again after changing clips. */
1985
- define() {
1986
- this._define();
1987
- return this;
1988
- }
1989
- _define() {
1990
- const node = this.node;
1991
- const tex = node.texture;
1992
- if (!tex) throw new Error("SpriteAnimation: set a texture on the Sprite before adding the aspect");
1993
- const [fw, fh] = this.size ?? [tex.width, tex.height];
1916
+ // --- animation (chainable) ---
1917
+ /**
1918
+ * Define named sprite-sheet clips from a uniform grid. Frame indices are row-major into the grid.
1919
+ * Sets the sprite's display size to one cell. Once defined, clips advance entirely in native code.
1920
+ */
1921
+ clips(def) {
1922
+ const tex = this._texture;
1923
+ if (!tex) throw new Error("Sprite.clips(): set a texture before defining clips");
1924
+ const [fw, fh] = def.size;
1925
+ const defFps = typeof def.fps === "number" ? def.fps : 12;
1926
+ const defLoop = typeof def.loop === "boolean" ? def.loop : true;
1994
1927
  const cols = Math.max(1, Math.floor(tex.width / fw));
1995
- for (const name of Object.keys(this.clips)) {
1996
- const value = this.clips[name];
1928
+ for (const name of Object.keys(def)) {
1929
+ if (name === "size" || name === "fps" || name === "loop") continue;
1930
+ const value = def[name];
1997
1931
  const indices = Array.isArray(value) ? value : value.frames;
1998
- const fps = Array.isArray(value) ? this.fps : value.fps ?? this.fps;
1999
- const loop = Array.isArray(value) ? this.loop : value.loop ?? this.loop;
1932
+ const fps = Array.isArray(value) ? defFps : value.fps ?? defFps;
1933
+ const loop = Array.isArray(value) ? defLoop : value.loop ?? defLoop;
2000
1934
  const frames = new Float32Array(indices.length * 4);
2001
1935
  indices.forEach((idx, i) => {
2002
- const px = idx % cols * fw;
2003
- const py = Math.floor(idx / cols) * fh;
2004
- frames[i * 4 + 0] = px / tex.width;
2005
- frames[i * 4 + 1] = py / tex.height;
2006
- frames[i * 4 + 2] = (px + fw) / tex.width;
2007
- frames[i * 4 + 3] = (py + fh) / tex.height;
1936
+ const cx2 = idx % cols * fw;
1937
+ const cy2 = Math.floor(idx / cols) * fh;
1938
+ frames[i * 4 + 0] = cx2 / tex.width;
1939
+ frames[i * 4 + 1] = cy2 / tex.height;
1940
+ frames[i * 4 + 2] = (cx2 + fw) / tex.width;
1941
+ frames[i * 4 + 3] = (cy2 + fh) / tex.height;
2008
1942
  });
2009
- const clipId = _creator2d.defineAnimation(node.id, frames, fps, loop);
1943
+ const clipId = _creator2d.defineAnimation(this.id, frames, fps, loop);
2010
1944
  this._clipIds[name] = clipId;
2011
1945
  this._clipNames[clipId] = name;
2012
1946
  }
2013
- if (this.size) _creator2d.setSpriteSize(node.id, fw, fh);
1947
+ if (!this._size) _creator2d.setSpriteSize(this.id, fw, fh);
1948
+ ensureAnimEvents();
1949
+ return this;
2014
1950
  }
2015
1951
  /** Play a clip by name. Re-playing the active clip is a no-op (keeps it running smoothly). */
2016
1952
  play(name) {
2017
1953
  const id = this._clipIds[name];
2018
- if (id === void 0) throw new Error(`SpriteAnimation.play(): unknown clip "${name}"`);
1954
+ if (id === void 0) throw new Error(`Sprite.play(): unknown clip "${name}"`);
2019
1955
  if (this._current === name) return this;
2020
1956
  this._current = name;
2021
- _creator2d.playAnimation(this.node.id, id);
1957
+ _creator2d.playAnimation(this.id, id);
2022
1958
  return this;
2023
1959
  }
2024
1960
  stop() {
2025
1961
  this._current = null;
2026
- _creator2d.stopAnimation(this.node.id);
1962
+ _creator2d.stopAnimation(this.id);
2027
1963
  return this;
2028
1964
  }
2029
- set speed(value) {
2030
- _creator2d.setAnimationSpeed(this.node.id, value);
1965
+ set animationSpeed(value) {
1966
+ _creator2d.setAnimationSpeed(this.id, value);
2031
1967
  }
2032
- get current() {
1968
+ get currentClip() {
2033
1969
  return this._current;
2034
1970
  }
2035
- get frame() {
2036
- return _creator2d.getAnimationFrame(this.node.id);
1971
+ get animationFrame() {
1972
+ return _creator2d.getAnimationFrame(this.id);
1973
+ }
1974
+ /** @internal — translate a native clip id back to its name and emit the animation event. */
1975
+ _dispatchAnimEvent(clipId, type) {
1976
+ const name = this._clipNames[clipId] ?? "";
1977
+ this.dispatch(type === 1 ? "completed" : "loopReached", name);
2037
1978
  }
2038
- };
2039
-
2040
- // src/g2/Collider2D.ts
2041
- var Collider2D = class extends Aspect {
2042
- static aspect = "collider";
2043
- /** Box half-extents (world units). Defaults to the sprite size when omitted. */
2044
- box;
2045
- /** Center offset from the node. */
2046
- offset;
2047
- /** Report overlaps via the node's 'enter'/'exit' events instead of blocking (always true for now). */
2048
- trigger = true;
2049
1979
  };
2050
1980
 
2051
1981
  // src/g2/Tilemap.ts
@@ -3226,6 +3156,82 @@ var QRScanner = class _QRScanner {
3226
3156
  }
3227
3157
  };
3228
3158
 
3159
+ // src/gl/Body.ts
3160
+ var MOTION = { static: 0, kinematic: 1, dynamic: 2 };
3161
+ var Body = class {
3162
+ _bodyId = 0;
3163
+ _motion;
3164
+ constructor(node, options = {}) {
3165
+ this._motion = options.motion ?? "dynamic";
3166
+ const mass = options.mass ?? 1;
3167
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return;
3168
+ const motion = MOTION[this._motion];
3169
+ const shape = resolveShape(node, options.shape ?? { type: "auto" });
3170
+ if (shape.type === "sphere") {
3171
+ this._bodyId = _creator.physicsCreateSphere(node.id, shape.radius, motion, mass);
3172
+ } else if (shape.type === "cylinder") {
3173
+ this._bodyId = _creator.physicsCreateCylinder(node.id, shape.halfHeight, shape.radius, motion, mass);
3174
+ } else {
3175
+ this._bodyId = _creator.physicsCreateBox(node.id, cx(shape.halfExtents), cy(shape.halfExtents), cz(shape.halfExtents), motion, mass);
3176
+ }
3177
+ }
3178
+ /** Native Jolt body id. 0 until assigned, or if the build has no physics support. */
3179
+ get id() {
3180
+ return this._bodyId;
3181
+ }
3182
+ get motion() {
3183
+ return this._motion;
3184
+ }
3185
+ /** Linear velocity in world units/second (fresh Vec3 on read). */
3186
+ get velocity() {
3187
+ const out = new Float32Array(3);
3188
+ if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out);
3189
+ return new Vec3(out[0], out[1], out[2]);
3190
+ }
3191
+ set velocity(v) {
3192
+ if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v));
3193
+ }
3194
+ /** Apply an instantaneous impulse (kg·m/s) and wake the body. */
3195
+ applyImpulse(v) {
3196
+ if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v));
3197
+ return this;
3198
+ }
3199
+ /** Teleport / drive a kinematic (or dynamic) body to a world position. */
3200
+ moveTo(p) {
3201
+ if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p));
3202
+ return this;
3203
+ }
3204
+ /** Remove the body from the simulation. */
3205
+ remove() {
3206
+ if (this._bodyId) {
3207
+ _creator.physicsRemoveBody(this._bodyId);
3208
+ this._bodyId = 0;
3209
+ }
3210
+ }
3211
+ };
3212
+ var resolveShape = (node, shape) => {
3213
+ if (shape.type !== "auto") return shape;
3214
+ const scale = node.worldScale;
3215
+ let hx = 0.5, hy = 0.5, hz = 0.5;
3216
+ const geom = node.geometry;
3217
+ if (geom) {
3218
+ const v = geom.vertices;
3219
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
3220
+ for (let i = 0; i + 2 < v.length; i += 3) {
3221
+ if (v[i] < minX) minX = v[i];
3222
+ if (v[i] > maxX) maxX = v[i];
3223
+ if (v[i + 1] < minY) minY = v[i + 1];
3224
+ if (v[i + 1] > maxY) maxY = v[i + 1];
3225
+ if (v[i + 2] < minZ) minZ = v[i + 2];
3226
+ if (v[i + 2] > maxZ) maxZ = v[i + 2];
3227
+ }
3228
+ hx = (maxX - minX) * 0.5;
3229
+ hy = (maxY - minY) * 0.5;
3230
+ hz = (maxZ - minZ) * 0.5;
3231
+ }
3232
+ return { type: "box", halfExtents: [hx * scale.x, hy * scale.y, hz * scale.z] };
3233
+ };
3234
+
3229
3235
  // src/gl/state.ts
3230
3236
  var glState = { activeScene: null };
3231
3237
 
@@ -3279,9 +3285,17 @@ var registerTouchEndEvent = () => {
3279
3285
 
3280
3286
  // src/gl/Node.ts
3281
3287
  var nodeRegistry = new Registry();
3282
- var Node = class _Node extends AspectHost {
3288
+ var animatorRegistered = false;
3289
+ var ensureAnimator = () => {
3290
+ if (animatorRegistered) return;
3291
+ animatorRegistered = true;
3292
+ _creator.setOnAnimationComplete((entityId, clipIndex) => nodeRegistry.get(entityId)?._onAnimationComplete(clipIndex) ?? true);
3293
+ };
3294
+ var Node = class _Node extends Emitter {
3283
3295
  /** Native entity handle. */
3284
3296
  id;
3297
+ /** Physics handle once .physics() is called. */
3298
+ body;
3285
3299
  /** AR anchors only — whether the anchor is currently being tracked. Kept in step with the
3286
3300
  * 'track' / 'untrack' events; `false` for any non-anchor node. */
3287
3301
  isTracked = false;
@@ -3289,8 +3303,10 @@ var Node = class _Node extends AspectHost {
3289
3303
  _lastSync = 0;
3290
3304
  _worldMatrix;
3291
3305
  _scaleCache;
3292
- /** @internal — the active animation aspect's clip-complete handler (installed by ModelAnimation). */
3293
- _animComplete;
3306
+ _animPlaying = false;
3307
+ _animClip = 0;
3308
+ _animSpeed = 1;
3309
+ _animLoop = false;
3294
3310
  constructor(internalId) {
3295
3311
  super();
3296
3312
  this.id = internalId ?? _creator.createEntity();
@@ -3460,7 +3476,24 @@ var Node = class _Node extends AspectHost {
3460
3476
  callback(this);
3461
3477
  _creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new _Node(id)));
3462
3478
  }
3463
- // --- behaviors are aspects: node.aspect(Physics, …) / node.aspect(Collider, …) ---
3479
+ // --- behaviors ---
3480
+ /** Add a physics body (reach it at node.body). Chainable. */
3481
+ physics(options = {}) {
3482
+ this.body = new Body(this, options);
3483
+ return this;
3484
+ }
3485
+ /** Add a collider for click/touch hit-testing. Derives from the mesh by default. Chainable. */
3486
+ collider(options = {}) {
3487
+ const form = options.shape ?? "box";
3488
+ if (form === "box" && options.center && options.size) {
3489
+ _creator.setColliderBox(this.id, cx(options.center), cy(options.center), cz(options.center), cx(options.size), cy(options.size), cz(options.size));
3490
+ } else if (form === "sphere" && options.center && options.radius !== void 0) {
3491
+ _creator.setColliderSphere(this.id, cx(options.center), cy(options.center), cz(options.center), options.radius);
3492
+ } else {
3493
+ _creator.setColliderFromMesh(this.id, this.id, form === "sphere" ? 1 : 0);
3494
+ }
3495
+ return this;
3496
+ }
3464
3497
  /** Orient the node so the given axis (default "-z", "forward") points at a world point. */
3465
3498
  lookAt(point, mode = "-z", ortho = [0, 1, 0]) {
3466
3499
  const world = this.worldMatrix;
@@ -3509,6 +3542,52 @@ var Node = class _Node extends AspectHost {
3509
3542
  );
3510
3543
  return this;
3511
3544
  }
3545
+ // --- GLB animation (no-op until a GLB is loaded as this node) ---
3546
+ get clips() {
3547
+ return _creator.getGlbAnimations(this.id);
3548
+ }
3549
+ get animationPlaying() {
3550
+ return this._animPlaying;
3551
+ }
3552
+ set animationPlaying(value) {
3553
+ this._animPlaying = value;
3554
+ _creator.setAnimation(this.id, value, this._animClip, this._animSpeed);
3555
+ }
3556
+ /** Play a GLB clip by name or index. Chainable. */
3557
+ play(clip, options = {}) {
3558
+ if (typeof clip === "number") this._animClip = clip;
3559
+ else if (typeof clip === "string") {
3560
+ const i = this.clips.findIndex((c2) => c2.name === clip);
3561
+ if (i >= 0) this._animClip = i;
3562
+ }
3563
+ if (options.loop !== void 0) this._animLoop = options.loop;
3564
+ this.animationPlaying = true;
3565
+ ensureAnimator();
3566
+ return this;
3567
+ }
3568
+ stop() {
3569
+ this.animationPlaying = false;
3570
+ return this;
3571
+ }
3572
+ get animationLoop() {
3573
+ return this._animLoop;
3574
+ }
3575
+ set animationLoop(v) {
3576
+ this._animLoop = v;
3577
+ }
3578
+ get animationSpeed() {
3579
+ return this._animSpeed;
3580
+ }
3581
+ set animationSpeed(v) {
3582
+ this._animSpeed = v;
3583
+ _creator.setAnimation(this.id, this._animPlaying, this._animClip, v);
3584
+ }
3585
+ get animationTime() {
3586
+ return _creator.getAnimationTime(this.id);
3587
+ }
3588
+ set animationTime(v) {
3589
+ _creator.setAnimationTime(this.id, this._animClip, v);
3590
+ }
3512
3591
  // --- events ---
3513
3592
  addEventListener(channel, callback) {
3514
3593
  if (channel === "click") registerTouchEndEvent();
@@ -3519,14 +3598,14 @@ var Node = class _Node extends AspectHost {
3519
3598
  _dispatchEvent(channel, ...args) {
3520
3599
  this.dispatch(channel, ...args);
3521
3600
  }
3522
- /** @internal — host animation-complete callback; forwards to the ModelAnimation aspect (if any).
3523
- * Returns whether the clip should keep playing. */
3601
+ /** @internal — host animation-complete callback; returns whether the clip should keep playing. */
3524
3602
  _onAnimationComplete(clipIndex) {
3525
- return this._animComplete?.(clipIndex) ?? true;
3526
- }
3527
- /** @internal — emit a GLB animation event (the ModelAnimation aspect can't reach protected dispatch). */
3528
- _emitAnim(channel, clip) {
3529
- this.dispatch(channel, clip);
3603
+ if (clipIndex !== this._animClip) return true;
3604
+ if (!this._animLoop) {
3605
+ this._animPlaying = false;
3606
+ this.dispatch("completed", clipIndex);
3607
+ } else this.dispatch("loopReached", clipIndex);
3608
+ return this._animPlaying;
3530
3609
  }
3531
3610
  destroy() {
3532
3611
  nodeRegistry.delete(this.id);
@@ -4087,10 +4166,10 @@ var createPlane = (options = {}) => {
4087
4166
  let upVector, rightVec;
4088
4167
  if (normal[1] === 0 && normal[2] === 0) {
4089
4168
  upVector = [0, 1, 0];
4090
- rightVec = [0, 0, -normal[0]];
4169
+ rightVec = [0, 0, normal[0]];
4091
4170
  } else if (normal[0] === 0 && normal[2] === 0) {
4092
4171
  upVector = [0, 0, 1];
4093
- rightVec = [-normal[1], 0, 0];
4172
+ rightVec = [normal[1], 0, 0];
4094
4173
  } else if (normal[0] === 0 && normal[1] === 0) {
4095
4174
  upVector = [0, 1, 0];
4096
4175
  rightVec = [normal[2], 0, 0];
@@ -4180,101 +4259,15 @@ var Mesh = class _Mesh extends Node {
4180
4259
  static from(geometry, options = {}) {
4181
4260
  return apply(new _Mesh(geometry, options.material), options);
4182
4261
  }
4183
- };
4184
- var sizeToVec = (s) => typeof s === "number" ? [s, s, s] : [cx(s), cy(s), cz(s)];
4185
- var apply = (mesh, o) => {
4186
- if (o.position) mesh.position = o.position;
4187
- if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles;
4188
- if (o.scale !== void 0) mesh.scale = typeof o.scale === "number" ? [o.scale, o.scale, o.scale] : o.scale;
4189
- if (o.name !== void 0) mesh.name = o.name;
4190
- if (o.castShadows !== void 0) mesh.castShadows = o.castShadows;
4191
- if (o.receiveShadows !== void 0) mesh.receiveShadows = o.receiveShadows;
4192
- return mesh;
4193
- };
4194
-
4195
- // src/gl/ModelAnimation.ts
4196
- var animatorRegistered = false;
4197
- var ensureAnimator = () => {
4198
- if (animatorRegistered) return;
4199
- animatorRegistered = true;
4200
- _creator.setOnAnimationComplete((entityId, clipIndex) => nodeRegistry.get(entityId)?._onAnimationComplete(clipIndex) ?? true);
4201
- };
4202
- var ModelAnimation = class extends Aspect {
4203
- static aspect = "anim";
4204
- _playing = false;
4205
- _clip = 0;
4206
- _speed = 1;
4207
- _loop = false;
4208
- onAttach() {
4209
- this.node._animComplete = (clipIndex) => {
4210
- if (clipIndex !== this._clip) return true;
4211
- if (!this._loop) {
4212
- this._playing = false;
4213
- this.node._emitAnim("completed", clipIndex);
4214
- } else this.node._emitAnim("loopReached", clipIndex);
4215
- return this._playing;
4216
- };
4217
- }
4218
- /** The clips baked into the glb. */
4219
- get clips() {
4220
- return _creator.getGlbAnimations(this.node.id);
4221
- }
4222
- get playing() {
4223
- return this._playing;
4224
- }
4225
- set playing(value) {
4226
- this._playing = value;
4227
- _creator.setAnimation(this.node.id, value, this._clip, this._speed);
4228
- }
4229
- /** Play a clip by name or index. Chainable. */
4230
- play(clip, options = {}) {
4231
- if (typeof clip === "number") this._clip = clip;
4232
- else if (typeof clip === "string") {
4233
- const i = this.clips.findIndex((c2) => c2.name === clip);
4234
- if (i >= 0) this._clip = i;
4235
- }
4236
- if (options.loop !== void 0) this._loop = options.loop;
4237
- this.playing = true;
4238
- ensureAnimator();
4239
- return this;
4240
- }
4241
- stop() {
4242
- this.playing = false;
4243
- return this;
4244
- }
4245
- get loop() {
4246
- return this._loop;
4247
- }
4248
- set loop(v) {
4249
- this._loop = v;
4250
- }
4251
- get speed() {
4252
- return this._speed;
4253
- }
4254
- set speed(v) {
4255
- this._speed = v;
4256
- _creator.setAnimation(this.node.id, this._playing, this._clip, v);
4257
- }
4258
- get time() {
4259
- return _creator.getAnimationTime(this.node.id);
4260
- }
4261
- set time(v) {
4262
- _creator.setAnimationTime(this.node.id, this._clip, v);
4263
- }
4264
- };
4265
-
4266
- // src/gl/Model.ts
4267
- var Model = class _Model extends Node {
4268
- constructor(internalId) {
4269
- super(internalId);
4270
- this.aspect(ModelAnimation);
4271
- }
4272
- /** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
4262
+ /**
4263
+ * Load a GLB model. Returns the model's root Node (a GLB is a node hierarchy); its animation clips
4264
+ * are available via node.clips and driven with node.play(name|index).
4265
+ */
4273
4266
  static load(source, options = {}) {
4274
4267
  if (typeof source === "string") {
4275
4268
  return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
4276
4269
  if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`));
4277
- return _Model.load(resp, options);
4270
+ return _Mesh.load(resp, options);
4278
4271
  });
4279
4272
  }
4280
4273
  return new Promise((resolve, reject) => {
@@ -4283,109 +4276,22 @@ var Model = class _Model extends Node {
4283
4276
  reject(new Error("Failed to load GLB"));
4284
4277
  return;
4285
4278
  }
4286
- const model = new _Model(entityId);
4279
+ const node = new Node(entityId);
4287
4280
  if (options.culling !== true) _creator.setGlbCulling(entityId, false);
4288
- resolve(model);
4281
+ resolve(node);
4289
4282
  }, reject);
4290
4283
  });
4291
4284
  }
4292
4285
  };
4293
-
4294
- // src/gl/Physics.ts
4295
- var MOTION = { static: 0, kinematic: 1, dynamic: 2 };
4296
- var Physics = class extends Aspect {
4297
- static aspect = "physics";
4298
- /** Default "dynamic". */
4299
- motion = "dynamic";
4300
- /** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
4301
- mass = 1;
4302
- /** Default { type: "auto" } — a box from the mesh AABB × world scale. */
4303
- shape = { type: "auto" };
4304
- _bodyId = 0;
4305
- onAttach() {
4306
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return;
4307
- const motion = MOTION[this.motion];
4308
- const shape = resolveShape(this.node, this.shape);
4309
- if (shape.type === "sphere") {
4310
- this._bodyId = _creator.physicsCreateSphere(this.node.id, shape.radius, motion, this.mass);
4311
- } else if (shape.type === "cylinder") {
4312
- this._bodyId = _creator.physicsCreateCylinder(this.node.id, shape.halfHeight, shape.radius, motion, this.mass);
4313
- } else {
4314
- this._bodyId = _creator.physicsCreateBox(this.node.id, cx(shape.halfExtents), cy(shape.halfExtents), cz(shape.halfExtents), motion, this.mass);
4315
- }
4316
- }
4317
- /** Native Jolt body id. 0 until attached, or if the build has no physics support. */
4318
- get id() {
4319
- return this._bodyId;
4320
- }
4321
- /** Linear velocity in world units/second (fresh Vec3 on read). */
4322
- get velocity() {
4323
- const out = new Float32Array(3);
4324
- if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out);
4325
- return new Vec3(out[0], out[1], out[2]);
4326
- }
4327
- set velocity(v) {
4328
- if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v));
4329
- }
4330
- /** Apply an instantaneous impulse (kg·m/s) and wake the body. */
4331
- applyImpulse(v) {
4332
- if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v));
4333
- return this;
4334
- }
4335
- /** Teleport / drive a kinematic (or dynamic) body to a world position. */
4336
- moveTo(p) {
4337
- if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p));
4338
- return this;
4339
- }
4340
- onDetach() {
4341
- if (this._bodyId) {
4342
- _creator.physicsRemoveBody(this._bodyId);
4343
- this._bodyId = 0;
4344
- }
4345
- }
4346
- };
4347
- var resolveShape = (node, shape) => {
4348
- if (shape.type !== "auto") return shape;
4349
- const scale = node.worldScale;
4350
- let hx = 0.5, hy = 0.5, hz = 0.5;
4351
- const geom = node.geometry;
4352
- if (geom) {
4353
- const v = geom.vertices;
4354
- let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
4355
- for (let i = 0; i + 2 < v.length; i += 3) {
4356
- if (v[i] < minX) minX = v[i];
4357
- if (v[i] > maxX) maxX = v[i];
4358
- if (v[i + 1] < minY) minY = v[i + 1];
4359
- if (v[i + 1] > maxY) maxY = v[i + 1];
4360
- if (v[i + 2] < minZ) minZ = v[i + 2];
4361
- if (v[i + 2] > maxZ) maxZ = v[i + 2];
4362
- }
4363
- hx = (maxX - minX) * 0.5;
4364
- hy = (maxY - minY) * 0.5;
4365
- hz = (maxZ - minZ) * 0.5;
4366
- }
4367
- return { type: "box", halfExtents: [hx * scale.x, hy * scale.y, hz * scale.z] };
4368
- };
4369
-
4370
- // src/gl/Collider.ts
4371
- var Collider = class extends Aspect {
4372
- static aspect = "collider";
4373
- shape = "box";
4374
- /** Explicit box: center + full size. */
4375
- center;
4376
- size;
4377
- /** Explicit sphere: center + radius. */
4378
- radius;
4379
- onAttach() {
4380
- const id = this.node.id;
4381
- if (this.shape === "box" && this.center && this.size) {
4382
- _creator.setColliderBox(id, cx(this.center), cy(this.center), cz(this.center), cx(this.size), cy(this.size), cz(this.size));
4383
- } else if (this.shape === "sphere" && this.center && this.radius !== void 0) {
4384
- _creator.setColliderSphere(id, cx(this.center), cy(this.center), cz(this.center), this.radius);
4385
- } else {
4386
- _creator.setColliderFromMesh(id, id, this.shape === "sphere" ? 1 : 0);
4387
- }
4388
- }
4286
+ var sizeToVec = (s) => typeof s === "number" ? [s, s, s] : [cx(s), cy(s), cz(s)];
4287
+ var apply = (mesh, o) => {
4288
+ if (o.position) mesh.position = o.position;
4289
+ if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles;
4290
+ if (o.scale !== void 0) mesh.scale = typeof o.scale === "number" ? [o.scale, o.scale, o.scale] : o.scale;
4291
+ if (o.name !== void 0) mesh.name = o.name;
4292
+ if (o.castShadows !== void 0) mesh.castShadows = o.castShadows;
4293
+ if (o.receiveShadows !== void 0) mesh.receiveShadows = o.receiveShadows;
4294
+ return mesh;
4389
4295
  };
4390
4296
 
4391
4297
  // src/gl/Light.ts
@@ -4654,12 +4560,9 @@ var dynamic = (baseValue, type, ...args) => {
4654
4560
  };
4655
4561
  export {
4656
4562
  ARScene,
4657
- Aspect,
4658
4563
  AudioPlayer,
4659
4564
  Camera,
4660
4565
  ClickEvent,
4661
- Collider,
4662
- Collider2D,
4663
4566
  Color,
4664
4567
  File,
4665
4568
  FormData,
@@ -4670,13 +4573,10 @@ export {
4670
4573
  Material,
4671
4574
  Mathf,
4672
4575
  Mesh,
4673
- Model,
4674
- ModelAnimation,
4675
4576
  Node,
4676
4577
  Node2D,
4677
4578
  Noise,
4678
4579
  Particles,
4679
- Physics,
4680
4580
  Plane,
4681
4581
  QRScanner,
4682
4582
  Quat,
@@ -4685,7 +4585,6 @@ export {
4685
4585
  Scene,
4686
4586
  Scene2D,
4687
4587
  Sprite,
4688
- SpriteAnimation,
4689
4588
  SvgSource,
4690
4589
  Texture,
4691
4590
  Texture2D,