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.
- package/dist/index.js +3 -3
- package/package.json +3 -3
- package/runtime/sdk/gl/CharacterController.ts +3 -0
- package/runtime/sdk/gl/Material.ts +12 -0
- package/runtime/sdk/gl/Particles.ts +676 -625
- package/runtime/sdk/gl/Physics.ts +126 -119
- package/runtime/sdk/gl/Scene.ts +299 -291
- package/runtime/sdk/gl/Shape.ts +37 -1
- package/runtime/sdk/gl/Trigger.ts +50 -46
- package/runtime/sdk-types.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
+
}
|