lecodes-sdk 0.20.0 → 1.0.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.
Files changed (114) hide show
  1. package/dist/global.d.ts +48 -5
  2. package/dist/inject.js +361 -260
  3. package/dist/types/audio/Bus.d.ts +45 -0
  4. package/dist/types/audio/Sound.d.ts +28 -0
  5. package/dist/types/audio/Voice.d.ts +27 -0
  6. package/dist/types/audio/audio.d.ts +83 -0
  7. package/dist/types/audio/support.d.ts +1 -0
  8. package/dist/types/canvas/Canvas.d.ts +2 -0
  9. package/dist/types/gl/DecalSet.d.ts +148 -0
  10. package/dist/types/gl/Geometry.d.ts +17 -0
  11. package/dist/types/gl/Light.d.ts +7 -0
  12. package/dist/types/gl/Lightmap.d.ts +9 -0
  13. package/dist/types/gl/Material.d.ts +90 -2
  14. package/dist/types/gl/Mesh.d.ts +18 -1
  15. package/dist/types/gl/Model.d.ts +34 -0
  16. package/dist/types/gl/Particles.d.ts +13 -0
  17. package/dist/types/gl/Scene.d.ts +23 -0
  18. package/dist/types/gl/Texture.d.ts +29 -1
  19. package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
  20. package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
  21. package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
  22. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  23. package/dist/types/gl/animation/core.d.ts +15 -10
  24. package/dist/types/gl/audio/AudioSource.d.ts +60 -0
  25. package/dist/types/gl/audio/AudioZone.d.ts +32 -0
  26. package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
  27. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  28. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  29. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  30. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  31. package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
  32. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  33. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  34. package/dist/types/gl/state.d.ts +0 -1
  35. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
  36. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  37. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  38. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  39. package/dist/types/inject.d.ts +33 -21
  40. package/dist/types/runtime/files.d.ts +24 -1
  41. package/dist/types/runtime/input.d.ts +11 -0
  42. package/dist/types/scene/defineScene.d.ts +10 -3
  43. package/dist/types/ui/UIImage.d.ts +15 -5
  44. package/dist/types.json +1 -1
  45. package/package.json +1 -1
  46. package/prompts/README.md +142 -142
  47. package/prompts/dist/2d-game.md +408 -197
  48. package/prompts/dist/3d-app.md +491 -166
  49. package/prompts/dist/ar-app.md +373 -163
  50. package/prompts/dist/design.md +83 -87
  51. package/prompts/dist/ui-app.md +325 -136
  52. package/src/audio/Bus.ts +102 -0
  53. package/src/audio/Sound.ts +96 -0
  54. package/src/audio/Voice.ts +102 -0
  55. package/src/audio/audio.ts +161 -0
  56. package/src/audio/support.ts +6 -0
  57. package/src/bridges.d.ts +279 -32
  58. package/src/canvas/Canvas.ts +21 -0
  59. package/src/compile/__tests__/compile.test.ts +11 -0
  60. package/src/compile/compileProject.ts +30 -15
  61. package/src/compile/header.ts +6 -3
  62. package/src/compile/index.ts +4 -0
  63. package/src/compile/sceneEditor.ts +42 -1
  64. package/src/core/Aspect.ts +33 -8
  65. package/src/g2/Scene2D.ts +7 -0
  66. package/src/gl/CameraPlace.ts +52 -52
  67. package/src/gl/DecalSet.ts +360 -0
  68. package/src/gl/Geometry.ts +348 -279
  69. package/src/gl/Light.ts +16 -0
  70. package/src/gl/Lightmap.ts +35 -7
  71. package/src/gl/Material.ts +173 -4
  72. package/src/gl/Mesh.ts +120 -83
  73. package/src/gl/Model.ts +33 -1
  74. package/src/gl/Node.ts +1 -1
  75. package/src/gl/Particles.ts +21 -3
  76. package/src/gl/Scene.ts +41 -7
  77. package/src/gl/Texture.ts +43 -3
  78. package/src/gl/animation/AnimationClip.ts +43 -20
  79. package/src/gl/animation/Animator.ts +4 -3
  80. package/src/gl/animation/DynamicBone.ts +459 -0
  81. package/src/gl/{IK.ts → animation/IK.ts} +4 -4
  82. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  83. package/src/gl/animation/core.ts +20 -15
  84. package/src/gl/audio/AudioSource.ts +113 -0
  85. package/src/gl/audio/AudioZone.ts +75 -0
  86. package/src/gl/audio/SceneAudio.ts +26 -0
  87. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  88. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  89. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  90. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  91. package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
  92. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  93. package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
  94. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  95. package/src/gl/scenarios.ts +291 -291
  96. package/src/gl/state.ts +1 -1
  97. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
  98. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  99. package/src/gl/vehicle/Vehicle.ts +666 -0
  100. package/src/gl/vehicle/Wheel.ts +290 -0
  101. package/src/inject.ts +226 -212
  102. package/src/runtime/files.ts +32 -2
  103. package/src/runtime/input.ts +6 -1
  104. package/src/scene/defineScene.ts +26 -10
  105. package/src/scene/gizmos.ts +148 -148
  106. package/src/scene/level.ts +2 -2
  107. package/src/ui/UIImage.ts +21 -7
  108. package/dist/types/gl/Gearbox.d.ts +0 -86
  109. package/dist/types/gl/Vehicle.d.ts +0 -191
  110. package/dist/types/gl/Wheel.d.ts +0 -95
  111. package/src/gl/Gearbox.ts +0 -212
  112. package/src/gl/Vehicle.ts +0 -473
  113. package/src/gl/Wheel.ts +0 -240
  114. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/dist/inject.js CHANGED
@@ -1235,6 +1235,88 @@ 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
+
1238
1320
  // src/runtime/misc.ts
1239
1321
  var toast = (msg) => _creatorUI.toast(msg);
1240
1322
  var SvgSource = (svg) => ({ svg, tintColor: null });
@@ -1336,36 +1418,6 @@ var Input = {
1336
1418
  }
1337
1419
  };
1338
1420
 
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
-
1369
1421
  // src/runtime/net.ts
1370
1422
  var WebSocket = class extends Emitter {
1371
1423
  _id = null;
@@ -1585,7 +1637,7 @@ var Registry = class {
1585
1637
 
1586
1638
  // src/g2/Node2D.ts
1587
1639
  var node2dRegistry = new Registry();
1588
- var Node2D = class extends Emitter {
1640
+ var Node2D = class extends AspectHost {
1589
1641
  /** Native entity handle. */
1590
1642
  id;
1591
1643
  _x = 0;
@@ -1665,17 +1717,15 @@ var Node2D = class extends Emitter {
1665
1717
  this._visible = v;
1666
1718
  _creator2d.setVisible(this.id, v);
1667
1719
  }
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;
1675
- }
1676
- /** @internal — overridden by Sprite to translate native clip ids into named animation events. */
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'. */
1677
1722
  _dispatchAnimEvent(_clipId, _type) {
1678
1723
  }
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);
1728
+ }
1679
1729
  destroy() {
1680
1730
  node2dRegistry.delete(this.id);
1681
1731
  _creator2d.destroyEntity(this.id);
@@ -1837,9 +1887,6 @@ var Sprite = class extends Node2D {
1837
1887
  _flipX = false;
1838
1888
  _flipY = false;
1839
1889
  _size = null;
1840
- _clipIds = {};
1841
- _clipNames = {};
1842
- _current = null;
1843
1890
  constructor(options = {}) {
1844
1891
  super();
1845
1892
  if (options.texture) this.texture = options.texture;
@@ -1913,71 +1960,94 @@ var Sprite = class extends Node2D {
1913
1960
  this._flipY = v;
1914
1961
  _creator2d.setSpriteFlip(this.id, this._flipX, v);
1915
1962
  }
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;
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];
1927
1994
  const cols = Math.max(1, Math.floor(tex.width / fw));
1928
- for (const name of Object.keys(def)) {
1929
- if (name === "size" || name === "fps" || name === "loop") continue;
1930
- const value = def[name];
1995
+ for (const name of Object.keys(this.clips)) {
1996
+ const value = this.clips[name];
1931
1997
  const indices = Array.isArray(value) ? value : value.frames;
1932
- const fps = Array.isArray(value) ? defFps : value.fps ?? defFps;
1933
- const loop = Array.isArray(value) ? defLoop : value.loop ?? defLoop;
1998
+ const fps = Array.isArray(value) ? this.fps : value.fps ?? this.fps;
1999
+ const loop = Array.isArray(value) ? this.loop : value.loop ?? this.loop;
1934
2000
  const frames = new Float32Array(indices.length * 4);
1935
2001
  indices.forEach((idx, i) => {
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;
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;
1942
2008
  });
1943
- const clipId = _creator2d.defineAnimation(this.id, frames, fps, loop);
2009
+ const clipId = _creator2d.defineAnimation(node.id, frames, fps, loop);
1944
2010
  this._clipIds[name] = clipId;
1945
2011
  this._clipNames[clipId] = name;
1946
2012
  }
1947
- if (!this._size) _creator2d.setSpriteSize(this.id, fw, fh);
1948
- ensureAnimEvents();
1949
- return this;
2013
+ if (this.size) _creator2d.setSpriteSize(node.id, fw, fh);
1950
2014
  }
1951
2015
  /** Play a clip by name. Re-playing the active clip is a no-op (keeps it running smoothly). */
1952
2016
  play(name) {
1953
2017
  const id = this._clipIds[name];
1954
- if (id === void 0) throw new Error(`Sprite.play(): unknown clip "${name}"`);
2018
+ if (id === void 0) throw new Error(`SpriteAnimation.play(): unknown clip "${name}"`);
1955
2019
  if (this._current === name) return this;
1956
2020
  this._current = name;
1957
- _creator2d.playAnimation(this.id, id);
2021
+ _creator2d.playAnimation(this.node.id, id);
1958
2022
  return this;
1959
2023
  }
1960
2024
  stop() {
1961
2025
  this._current = null;
1962
- _creator2d.stopAnimation(this.id);
2026
+ _creator2d.stopAnimation(this.node.id);
1963
2027
  return this;
1964
2028
  }
1965
- set animationSpeed(value) {
1966
- _creator2d.setAnimationSpeed(this.id, value);
2029
+ set speed(value) {
2030
+ _creator2d.setAnimationSpeed(this.node.id, value);
1967
2031
  }
1968
- get currentClip() {
2032
+ get current() {
1969
2033
  return this._current;
1970
2034
  }
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);
2035
+ get frame() {
2036
+ return _creator2d.getAnimationFrame(this.node.id);
1978
2037
  }
1979
2038
  };
1980
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
+ };
2050
+
1981
2051
  // src/g2/Tilemap.ts
1982
2052
  var Tilemap = class extends Node2D {
1983
2053
  cols;
@@ -3156,82 +3226,6 @@ var QRScanner = class _QRScanner {
3156
3226
  }
3157
3227
  };
3158
3228
 
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
-
3235
3229
  // src/gl/state.ts
3236
3230
  var glState = { activeScene: null };
3237
3231
 
@@ -3285,17 +3279,9 @@ var registerTouchEndEvent = () => {
3285
3279
 
3286
3280
  // src/gl/Node.ts
3287
3281
  var nodeRegistry = new Registry();
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 {
3282
+ var Node = class _Node extends AspectHost {
3295
3283
  /** Native entity handle. */
3296
3284
  id;
3297
- /** Physics handle once .physics() is called. */
3298
- body;
3299
3285
  /** AR anchors only — whether the anchor is currently being tracked. Kept in step with the
3300
3286
  * 'track' / 'untrack' events; `false` for any non-anchor node. */
3301
3287
  isTracked = false;
@@ -3303,10 +3289,8 @@ var Node = class _Node extends Emitter {
3303
3289
  _lastSync = 0;
3304
3290
  _worldMatrix;
3305
3291
  _scaleCache;
3306
- _animPlaying = false;
3307
- _animClip = 0;
3308
- _animSpeed = 1;
3309
- _animLoop = false;
3292
+ /** @internal — the active animation aspect's clip-complete handler (installed by ModelAnimation). */
3293
+ _animComplete;
3310
3294
  constructor(internalId) {
3311
3295
  super();
3312
3296
  this.id = internalId ?? _creator.createEntity();
@@ -3476,24 +3460,7 @@ var Node = class _Node extends Emitter {
3476
3460
  callback(this);
3477
3461
  _creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new _Node(id)));
3478
3462
  }
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
- }
3463
+ // --- behaviors are aspects: node.aspect(Physics, …) / node.aspect(Collider, …) ---
3497
3464
  /** Orient the node so the given axis (default "-z", "forward") points at a world point. */
3498
3465
  lookAt(point, mode = "-z", ortho = [0, 1, 0]) {
3499
3466
  const world = this.worldMatrix;
@@ -3542,52 +3509,6 @@ var Node = class _Node extends Emitter {
3542
3509
  );
3543
3510
  return this;
3544
3511
  }
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
- }
3591
3512
  // --- events ---
3592
3513
  addEventListener(channel, callback) {
3593
3514
  if (channel === "click") registerTouchEndEvent();
@@ -3598,14 +3519,14 @@ var Node = class _Node extends Emitter {
3598
3519
  _dispatchEvent(channel, ...args) {
3599
3520
  this.dispatch(channel, ...args);
3600
3521
  }
3601
- /** @internal — host animation-complete callback; returns whether the clip should keep playing. */
3522
+ /** @internal — host animation-complete callback; forwards to the ModelAnimation aspect (if any).
3523
+ * Returns whether the clip should keep playing. */
3602
3524
  _onAnimationComplete(clipIndex) {
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;
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);
3609
3530
  }
3610
3531
  destroy() {
3611
3532
  nodeRegistry.delete(this.id);
@@ -4166,10 +4087,10 @@ var createPlane = (options = {}) => {
4166
4087
  let upVector, rightVec;
4167
4088
  if (normal[1] === 0 && normal[2] === 0) {
4168
4089
  upVector = [0, 1, 0];
4169
- rightVec = [0, 0, normal[0]];
4090
+ rightVec = [0, 0, -normal[0]];
4170
4091
  } else if (normal[0] === 0 && normal[2] === 0) {
4171
4092
  upVector = [0, 0, 1];
4172
- rightVec = [normal[1], 0, 0];
4093
+ rightVec = [-normal[1], 0, 0];
4173
4094
  } else if (normal[0] === 0 && normal[1] === 0) {
4174
4095
  upVector = [0, 1, 0];
4175
4096
  rightVec = [normal[2], 0, 0];
@@ -4259,15 +4180,101 @@ var Mesh = class _Mesh extends Node {
4259
4180
  static from(geometry, options = {}) {
4260
4181
  return apply(new _Mesh(geometry, options.material), options);
4261
4182
  }
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
- */
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. */
4266
4273
  static load(source, options = {}) {
4267
4274
  if (typeof source === "string") {
4268
4275
  return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
4269
4276
  if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`));
4270
- return _Mesh.load(resp, options);
4277
+ return _Model.load(resp, options);
4271
4278
  });
4272
4279
  }
4273
4280
  return new Promise((resolve, reject) => {
@@ -4276,22 +4283,109 @@ var Mesh = class _Mesh extends Node {
4276
4283
  reject(new Error("Failed to load GLB"));
4277
4284
  return;
4278
4285
  }
4279
- const node = new Node(entityId);
4286
+ const model = new _Model(entityId);
4280
4287
  if (options.culling !== true) _creator.setGlbCulling(entityId, false);
4281
- resolve(node);
4288
+ resolve(model);
4282
4289
  }, reject);
4283
4290
  });
4284
4291
  }
4285
4292
  };
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;
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
+ }
4295
4389
  };
4296
4390
 
4297
4391
  // src/gl/Light.ts
@@ -4560,9 +4654,12 @@ var dynamic = (baseValue, type, ...args) => {
4560
4654
  };
4561
4655
  export {
4562
4656
  ARScene,
4657
+ Aspect,
4563
4658
  AudioPlayer,
4564
4659
  Camera,
4565
4660
  ClickEvent,
4661
+ Collider,
4662
+ Collider2D,
4566
4663
  Color,
4567
4664
  File,
4568
4665
  FormData,
@@ -4573,10 +4670,13 @@ export {
4573
4670
  Material,
4574
4671
  Mathf,
4575
4672
  Mesh,
4673
+ Model,
4674
+ ModelAnimation,
4576
4675
  Node,
4577
4676
  Node2D,
4578
4677
  Noise,
4579
4678
  Particles,
4679
+ Physics,
4580
4680
  Plane,
4581
4681
  QRScanner,
4582
4682
  Quat,
@@ -4585,6 +4685,7 @@ export {
4585
4685
  Scene,
4586
4686
  Scene2D,
4587
4687
  Sprite,
4688
+ SpriteAnimation,
4588
4689
  SvgSource,
4589
4690
  Texture,
4590
4691
  Texture2D,