lecodes-cli 0.17.1 → 0.17.2

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.
@@ -28,6 +28,11 @@ export class Shape extends Aspect<"shape", Node> {
28
28
  cylinder?: { halfHeight: number, radius: number }
29
29
  /** Capsule aligned on Y: half-height of the cylinder section + radius. */
30
30
  capsule?: { halfHeight: number, radius: number }
31
+ /** The node's own triangles (a Mesh's geometry or a Model's GLB, world scale applied).
32
+ * `true` = exact triangle mesh — static/kinematic bodies, triggers, characters' ground and picking
33
+ * only (a dynamic body can't be a triangle mesh; Physics throws). `'convex'` = convex hull of the
34
+ * vertices, works for dynamic bodies too. A Model uses its bind pose (skinned parts skipped). */
35
+ mesh?: boolean | "convex"
31
36
  /** Whether pointer rays can hit this shape (click / touchstart). Default true. */
32
37
  raycast = true
33
38
 
@@ -70,13 +75,44 @@ export class Shape extends Aspect<"shape", Node> {
70
75
  if (!this.raycast && this._bodyId) _creator.physicsSetPickable(this._bodyId, false)
71
76
  }
72
77
 
78
+ /** @internal true for an exact triangle mesh — Physics refuses a dynamic body on it. */
79
+ get _isTriangleMesh(): boolean { return this.mesh === true }
80
+
73
81
  private _build(): number {
82
+ if (this.mesh) return this._buildMesh(this.mesh === "convex")
74
83
  if (this.sphere !== undefined) return _creator.physicsBuildSphere(this.sphere)
75
84
  if (this.cylinder) return _creator.physicsBuildCylinder(this.cylinder.halfHeight, this.cylinder.radius)
76
85
  if (this.capsule) return _creator.physicsBuildCapsule(this.capsule.halfHeight, this.capsule.radius)
77
86
  if (this.box) return _creator.physicsBuildBox(cx(this.box), cy(this.box), cz(this.box))
87
+ return this._buildAutoBox()
88
+ }
89
+
90
+ // mesh: a Mesh node hands its CPU geometry over; anything else is tried as a GLB root (the host
91
+ // reads the asset's triangles itself — 0 if the node is neither). Always × world scale: the
92
+ // triangles are node-local, so the collider must follow the rendered size.
93
+ private _buildMesh(convex: boolean): number {
94
+ if (!_creator.physicsBuildMesh) {
95
+ console.warn("Shape { mesh } needs a newer host — falling back to a box")
96
+ return this._buildAutoBox()
97
+ }
98
+ const s = this.node.worldScale
99
+ const geom = this.node.geometry
100
+ let id = 0
101
+ if (geom) {
102
+ if (geom._meshKind !== 0) throw new Error("Shape { mesh } needs a triangle geometry (not edges/vertices)")
103
+ id = _creator.physicsBuildMesh(geom.vertices, Uint32Array.from(geom.indices), convex, s.x, s.y, s.z)
104
+ } else {
105
+ id = _creator.physicsBuildMeshFromEntity(this.node.id, convex, s.x, s.y, s.z)
106
+ }
107
+ if (!id) {
108
+ console.warn(`Shape { mesh } could not build a ${convex ? "convex hull" : "triangle mesh"} for "${this.node.name}" (no triangles / not a Mesh or Model) — falling back to a box`)
109
+ return this._buildAutoBox()
110
+ }
111
+ return id
112
+ }
78
113
 
79
- // auto: a box from the mesh AABB × world scale (so the physical box matches the rendered size).
114
+ // auto: a box from the mesh AABB × world scale (so the physical box matches the rendered size).
115
+ private _buildAutoBox(): number {
80
116
  const s = this.node.worldScale
81
117
  let hx = 0.5, hy = 0.5, hz = 0.5
82
118
  const geom = this.node.geometry
@@ -1,46 +1,50 @@
1
- // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
- // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
- // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
- //
5
- // const goal = new Mesh(box(), material)
6
- // .aspect(Shape, { box: [1, 2, 1] })
7
- // .aspect(Trigger)
8
- // goal.addEventListener('enter', other => win(other))
9
- // goal.addEventListener('exit', other => …)
10
-
11
- import { Aspect } from "../core/Aspect"
12
- import { cx, cy, cz, type Vec3Like } from "../math/vec"
13
- import { Shape } from "./Shape"
14
- import { ensurePhysicsEvents } from "./physicsEvents"
15
- import type { Node } from "./Node"
16
-
17
- export class Trigger extends Aspect<"trigger", Node> {
18
- static readonly aspect = "trigger"
19
-
20
- private _bodyId = 0
21
- /** Native body id (0 if no physics support). */
22
- get id(): number { return this._bodyId }
23
-
24
- onAttach(): void {
25
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
26
- const shape = this.node.get(Shape)
27
- if (!shape) {
28
- throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
29
- }
30
- const shapeId = shape._claim()
31
- this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
32
- shape._ownBody(this._bodyId)
33
- ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
34
- }
35
-
36
- onDetach(): void {
37
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
38
- this.node.get(Shape)?._recreatePickBody()
39
- }
40
-
41
- /** Reposition the trigger zone to a world point. */
42
- moveTo(p: Vec3Like): this {
43
- if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
44
- return this
45
- }
46
- }
1
+ // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
+ // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
+ // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
+ //
5
+ // const goal = new Mesh(box(), material)
6
+ // .aspect(Shape, { box: [1, 2, 1] })
7
+ // .aspect(Trigger)
8
+ // goal.addEventListener('enter', other => win(other))
9
+ // goal.addEventListener('exit', other => …)
10
+
11
+ import { Aspect } from "../core/Aspect"
12
+ import { cx, cy, cz, type Vec3Like } from "../math/vec"
13
+ import { Shape } from "./Shape"
14
+ import { ensurePhysicsEvents } from "./physicsEvents"
15
+ import type { Node } from "./Node"
16
+
17
+ export class Trigger extends Aspect<"trigger", Node> {
18
+ static readonly aspect = "trigger"
19
+
20
+ private _bodyId = 0
21
+ /** Native body id (0 if no physics support). */
22
+ get id(): number { return this._bodyId }
23
+
24
+ onAttach(): void {
25
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
26
+ const shape = this.node.get(Shape)
27
+ if (!shape) {
28
+ throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
29
+ }
30
+ const shapeId = shape._claim()
31
+ this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
32
+ shape._ownBody(this._bodyId)
33
+ ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
34
+ }
35
+
36
+ onDetach(): void {
37
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
38
+ const shape = this.node.get(Shape)
39
+ if (shape) {
40
+ shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
41
+ shape._recreatePickBody()
42
+ }
43
+ }
44
+
45
+ /** Reposition the trigger zone to a world point. */
46
+ moveTo(p: Vec3Like): this {
47
+ if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
48
+ return this
49
+ }
50
+ }