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.
@@ -1,119 +1,126 @@
1
- // Rigid body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl. Requires a Shape
2
- // aspect on the same node (its geometry becomes the body's collision shape).
3
- //
4
- // Physics.configure({ gravity: [0, -9.81, 0] }) // once, before creating bodies
5
- // ground.aspect(Shape, { box: [20, 0.5, 20] }).aspect(Physics, { motion: 'static' })
6
- // crate.aspect(Shape, {}).aspect(Physics, { mass: 2 }) // auto box from the mesh
7
- // crate.physics.applyImpulse([0, 6, 0])
8
- // crate.addEventListener('enter', other => …) // contact / trigger overlap began
9
- //
10
- // For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
11
- // velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic bodies
12
- // are driven with moveTo.
13
-
14
- import { Aspect } from "../core/Aspect"
15
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
16
- import { nodeRegistry, type Node } from "./Node"
17
- import { Shape } from "./Shape"
18
-
19
- export type MotionType = "static" | "dynamic" | "kinematic"
20
- const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
21
-
22
- export interface PhysicsConfig {
23
- /** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
24
- gravity?: Vec3Like
25
- /** Max simultaneous bodies (resizes the world; only before any body exists). Default 4096. */
26
- maxBodies?: number
27
- }
28
-
29
- /** Closest raycast hit. */
30
- export interface RayHit {
31
- node: Node | null
32
- point: Vec3
33
- normal: Vec3
34
- fraction: number
35
- }
36
-
37
- export class Physics extends Aspect<"physics", Node> {
38
- static readonly aspect = "physics"
39
-
40
- /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
41
- * off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
42
- * smoothness — bodies then advance in discrete steps. */
43
- private static _interpolation = true
44
- static get interpolation(): boolean { return Physics._interpolation }
45
- static set interpolation(on: boolean) { Physics._interpolation = on; _creator.setInterpolation(on) }
46
-
47
- /** Default "dynamic". */
48
- motion: MotionType = "dynamic"
49
- /** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
50
- mass = 1
51
-
52
- private _bodyId = 0
53
-
54
- onAttach(): void {
55
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
56
- const shape = this.node.get(Shape)
57
- if (!shape) {
58
- throw new Error("Physics requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Physics, {…})")
59
- }
60
- const shapeId = shape._claim() // drop the pick-only body; reuse its shape
61
- this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
62
- shape._ownBody(this._bodyId)
63
- }
64
-
65
- onDetach(): void {
66
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
67
- this.node.get(Shape)?._recreatePickBody() // keep the node pickable if the Shape stays
68
- }
69
-
70
- /** Native Jolt body id. 0 until attached, or if the build has no physics support. */
71
- get id(): number { return this._bodyId }
72
-
73
- /** Linear velocity in world units/second (fresh Vec3 on read). */
74
- get velocity(): Vec3 {
75
- const out = new Float32Array(3)
76
- if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
77
- return new Vec3(out[0], out[1], out[2])
78
- }
79
- set velocity(v: Vec3Like) {
80
- if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
81
- }
82
-
83
- /** Apply an instantaneous impulse (kg·m/s) and wake the body. */
84
- applyImpulse(v: Vec3Like): this {
85
- if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
86
- return this
87
- }
88
-
89
- /** Teleport / drive a kinematic (or dynamic) body to a world position. */
90
- moveTo(p: Vec3Like): this {
91
- if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
92
- return this
93
- }
94
-
95
- // --- world (static) -------------------------------------------------------------------------
96
- /** Configure gravity / limits. Call once before creating bodies. */
97
- static configure(config: PhysicsConfig = {}): void {
98
- const g = config.gravity ?? [0, -9.81, 0]
99
- _creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
100
- }
101
-
102
- /** Whether this build has physics support (CREATOR_GL_PHYSICS). */
103
- static get supported(): boolean {
104
- return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
105
- }
106
-
107
- /** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
108
- static raycast(origin: Vec3Like, dir: Vec3Like, maxDist = 1000): RayHit | null {
109
- const out = new Float32Array(7)
110
- const id = _creator.physicsRaycast(cx(origin), cy(origin), cz(origin), cx(dir), cy(dir), cz(dir), maxDist, out)
111
- if (!id) return null
112
- return {
113
- node: nodeRegistry.get(id) ?? null,
114
- point: new Vec3(out[0], out[1], out[2]),
115
- normal: new Vec3(out[3], out[4], out[5]),
116
- fraction: out[6],
117
- }
118
- }
119
- }
1
+ // Rigid body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl. Requires a Shape
2
+ // aspect on the same node (its geometry becomes the body's collision shape).
3
+ //
4
+ // Physics.configure({ gravity: [0, -9.81, 0] }) // once, before creating bodies
5
+ // ground.aspect(Shape, { box: [20, 0.5, 20] }).aspect(Physics, { motion: 'static' })
6
+ // crate.aspect(Shape, {}).aspect(Physics, { mass: 2 }) // auto box from the mesh
7
+ // crate.physics.applyImpulse([0, 6, 0])
8
+ // crate.addEventListener('enter', other => …) // contact / trigger overlap began
9
+ //
10
+ // For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
11
+ // velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic bodies
12
+ // are driven with moveTo.
13
+
14
+ import { Aspect } from "../core/Aspect"
15
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
16
+ import { nodeRegistry, type Node } from "./Node"
17
+ import { Shape } from "./Shape"
18
+
19
+ export type MotionType = "static" | "dynamic" | "kinematic"
20
+ const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
21
+
22
+ export interface PhysicsConfig {
23
+ /** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
24
+ gravity?: Vec3Like
25
+ /** Max simultaneous bodies (resizes the world; only before any body exists). Default 4096. */
26
+ maxBodies?: number
27
+ }
28
+
29
+ /** Closest raycast hit. */
30
+ export interface RayHit {
31
+ node: Node | null
32
+ point: Vec3
33
+ normal: Vec3
34
+ fraction: number
35
+ }
36
+
37
+ export class Physics extends Aspect<"physics", Node> {
38
+ static readonly aspect = "physics"
39
+
40
+ /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
41
+ * off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
42
+ * smoothness — bodies then advance in discrete steps. */
43
+ private static _interpolation = true
44
+ static get interpolation(): boolean { return Physics._interpolation }
45
+ static set interpolation(on: boolean) { Physics._interpolation = on; _creator.setInterpolation(on) }
46
+
47
+ /** Default "dynamic". */
48
+ motion: MotionType = "dynamic"
49
+ /** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
50
+ mass = 1
51
+
52
+ private _bodyId = 0
53
+
54
+ onAttach(): void {
55
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
56
+ const shape = this.node.get(Shape)
57
+ if (!shape) {
58
+ throw new Error("Physics requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Physics, {…})")
59
+ }
60
+ if (this.motion === "dynamic" && shape._isTriangleMesh) {
61
+ throw new Error("Physics: a dynamic body cannot use Shape { mesh: true } (triangle meshes are static/kinematic only) — use { mesh: 'convex' } or a primitive shape")
62
+ }
63
+ const shapeId = shape._claim() // drop the pick-only body; reuse its shape
64
+ this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
65
+ shape._ownBody(this._bodyId)
66
+ }
67
+
68
+ onDetach(): void {
69
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
70
+ const shape = this.node.get(Shape)
71
+ if (shape) {
72
+ shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
73
+ shape._recreatePickBody() // keep the node pickable if the Shape stays
74
+ }
75
+ }
76
+
77
+ /** Native Jolt body id. 0 until attached, or if the build has no physics support. */
78
+ get id(): number { return this._bodyId }
79
+
80
+ /** Linear velocity in world units/second (fresh Vec3 on read). */
81
+ get velocity(): Vec3 {
82
+ const out = new Float32Array(3)
83
+ if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
84
+ return new Vec3(out[0], out[1], out[2])
85
+ }
86
+ set velocity(v: Vec3Like) {
87
+ if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
88
+ }
89
+
90
+ /** Apply an instantaneous impulse (kg·m/s) and wake the body. */
91
+ applyImpulse(v: Vec3Like): this {
92
+ if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
93
+ return this
94
+ }
95
+
96
+ /** Teleport / drive a kinematic (or dynamic) body to a world position. */
97
+ moveTo(p: Vec3Like): this {
98
+ if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
99
+ return this
100
+ }
101
+
102
+ // --- world (static) -------------------------------------------------------------------------
103
+ /** Configure gravity / limits. Call once before creating bodies. */
104
+ static configure(config: PhysicsConfig = {}): void {
105
+ const g = config.gravity ?? [0, -9.81, 0]
106
+ _creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
107
+ }
108
+
109
+ /** Whether this build has physics support (CREATOR_GL_PHYSICS). */
110
+ static get supported(): boolean {
111
+ return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
112
+ }
113
+
114
+ /** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
115
+ static raycast(origin: Vec3Like, dir: Vec3Like, maxDist = 1000): RayHit | null {
116
+ const out = new Float32Array(7)
117
+ const id = _creator.physicsRaycast(cx(origin), cy(origin), cz(origin), cx(dir), cy(dir), cz(dir), maxDist, out)
118
+ if (!id) return null
119
+ return {
120
+ node: nodeRegistry.get(id) ?? null,
121
+ point: new Vec3(out[0], out[1], out[2]),
122
+ normal: new Vec3(out[3], out[4], out[5]),
123
+ fraction: out[6],
124
+ }
125
+ }
126
+ }