lecodes-cli 0.1.6 → 0.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/README.md +7 -1
- package/dist/index.js +202 -84
- package/package.json +4 -4
- package/runtime/sdk/animate/animate.ts +238 -0
- package/runtime/sdk/animate/bezier.ts +138 -0
- package/runtime/sdk/animate/easings.ts +126 -0
- package/runtime/sdk/core/Aspect.ts +151 -0
- package/runtime/sdk/core/color.ts +66 -0
- package/runtime/sdk/core/events.ts +38 -0
- package/runtime/sdk/core/registry.ts +23 -0
- package/runtime/sdk/g2/Camera2D.ts +33 -0
- package/runtime/sdk/g2/CharacterController2D.ts +75 -0
- package/runtime/sdk/g2/Node2D.ts +194 -0
- package/runtime/sdk/g2/Physics2D.ts +126 -0
- package/runtime/sdk/g2/Scene2D.ts +101 -0
- package/runtime/sdk/g2/Shape.ts +63 -0
- package/runtime/sdk/g2/Sprite.ts +79 -0
- package/runtime/sdk/g2/SpriteAnimation.ts +90 -0
- package/runtime/sdk/g2/Texture2D.ts +37 -0
- package/runtime/sdk/g2/Tilemap.ts +41 -0
- package/runtime/sdk/g2/Trigger.ts +47 -0
- package/runtime/sdk/g2/loop.ts +39 -0
- package/runtime/sdk/g2/touch.ts +83 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/gl/Collider.ts +29 -0
- package/runtime/sdk/gl/Geometry.ts +218 -0
- package/runtime/sdk/gl/Light.ts +37 -0
- package/runtime/sdk/gl/Material.ts +124 -0
- package/runtime/sdk/gl/Mesh.ts +100 -0
- package/runtime/sdk/gl/Model.ts +40 -0
- package/runtime/sdk/gl/ModelAnimation.ts +64 -0
- package/runtime/sdk/gl/Node.ts +234 -0
- package/runtime/sdk/gl/Noise.ts +30 -0
- package/runtime/sdk/gl/Particles.ts +185 -0
- package/runtime/sdk/gl/Physics.ts +95 -0
- package/runtime/sdk/gl/Plane.ts +53 -0
- package/runtime/sdk/gl/Ray.ts +16 -0
- package/runtime/sdk/gl/Scene.ts +179 -0
- package/runtime/sdk/gl/Texture.ts +35 -0
- package/runtime/sdk/gl/controls.ts +95 -0
- package/runtime/sdk/gl/state.ts +6 -0
- package/runtime/sdk/gl/touch.ts +56 -0
- package/runtime/sdk/inject.ts +85 -0
- package/runtime/sdk/math/Mathf.ts +118 -0
- package/runtime/sdk/math/mat4.ts +278 -0
- package/runtime/sdk/math/quat.ts +232 -0
- package/runtime/sdk/math/vec.ts +255 -0
- package/runtime/sdk/plugins/qr.ts +36 -0
- package/runtime/sdk/runtime/device.ts +33 -0
- package/runtime/sdk/runtime/fetch.ts +71 -0
- package/runtime/sdk/runtime/files.ts +23 -0
- package/runtime/sdk/runtime/input.ts +12 -0
- package/runtime/sdk/runtime/media.ts +111 -0
- package/runtime/sdk/runtime/misc.ts +12 -0
- package/runtime/sdk/runtime/net.ts +36 -0
- package/runtime/sdk/runtime/storage.ts +13 -0
- package/runtime/sdk/runtime/touch.ts +50 -0
- package/runtime/sdk/ui/UI.ts +19 -0
- package/runtime/sdk/ui/UIButton.ts +84 -0
- package/runtime/sdk/ui/UIContainer.ts +100 -0
- package/runtime/sdk/ui/UIImage.ts +68 -0
- package/runtime/sdk/ui/UIInput.ts +78 -0
- package/runtime/sdk/ui/UINode.ts +342 -0
- package/runtime/sdk/ui/UIScreen.ts +122 -0
- package/runtime/sdk/ui/UIScrollable.ts +70 -0
- package/runtime/sdk/ui/UISpacer.ts +14 -0
- package/runtime/sdk/ui/UIText.ts +40 -0
- package/runtime/sdk/ui/UIVideo.ts +38 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
- package/runtime/sdk/ui/UIWidget.ts +97 -0
- package/runtime/sdk/ui/fonts.ts +10 -0
- package/runtime/sdk/ui/router.ts +73 -0
- package/runtime/inject.js +0 -4629
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Base 3D scene node — an empty transform. Mesh and Light extend it, so every node operation works
|
|
2
|
+
// on them. Native (creator-gl) owns the authoritative transform; reads decompose a cached local
|
|
3
|
+
// matrix (re-synced lazily, since physics can write the transform from native each frame).
|
|
4
|
+
//
|
|
5
|
+
// Capabilities are aspects: node.aspect(Physics, …) (→ node.physics), node.aspect(Collider, …).
|
|
6
|
+
// A GLB import is the Model node kind (with the ModelAnimation aspect). Hierarchy lives here.
|
|
7
|
+
|
|
8
|
+
import { AspectHost } from "../core/Aspect"
|
|
9
|
+
import { Registry } from "../core/registry"
|
|
10
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
11
|
+
import { Quat, cw, type QuatLike } from "../math/quat"
|
|
12
|
+
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
13
|
+
import type { Geometry } from "./Geometry"
|
|
14
|
+
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
15
|
+
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
16
|
+
|
|
17
|
+
/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
|
|
18
|
+
export const nodeRegistry = new Registry<Node>()
|
|
19
|
+
|
|
20
|
+
export type NodeEvents = {
|
|
21
|
+
click: (ev: ClickEvent<Node | null>) => void
|
|
22
|
+
touchstart: (ev: TouchStartEvent<Node | null>) => void
|
|
23
|
+
/** A looping GLB animation clip wrapped around. */
|
|
24
|
+
loopReached: (clip: number) => void
|
|
25
|
+
/** A non-looping GLB animation clip finished. */
|
|
26
|
+
completed: (clip: number) => void
|
|
27
|
+
/** AR anchors only (`scene.root` / `scene.createAnchor()`): the anchor began tracking. */
|
|
28
|
+
track: () => void
|
|
29
|
+
/** AR anchors only: the anchor lost tracking. */
|
|
30
|
+
untrack: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Node extends AspectHost<NodeEvents> {
|
|
34
|
+
/** Native entity handle. */
|
|
35
|
+
readonly id: number
|
|
36
|
+
|
|
37
|
+
/** AR anchors only — whether the anchor is currently being tracked. Kept in step with the
|
|
38
|
+
* 'track' / 'untrack' events; `false` for any non-anchor node. */
|
|
39
|
+
isTracked = false
|
|
40
|
+
|
|
41
|
+
private _matrix?: Float32Array
|
|
42
|
+
private _lastSync = 0
|
|
43
|
+
private _worldMatrix?: Float32Array
|
|
44
|
+
private _scaleCache?: [number, number, number]
|
|
45
|
+
|
|
46
|
+
/** @internal — the active animation aspect's clip-complete handler (installed by ModelAnimation). */
|
|
47
|
+
_animComplete?: (clipIndex: number) => boolean
|
|
48
|
+
|
|
49
|
+
constructor(internalId?: number) {
|
|
50
|
+
super()
|
|
51
|
+
this.id = internalId ?? _creator.createEntity()
|
|
52
|
+
nodeRegistry.set(this.id, this)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Geometry, if this node is a Mesh (used by physics auto-shape). Overridden by Mesh. */
|
|
56
|
+
get geometry(): Geometry | undefined { return undefined }
|
|
57
|
+
|
|
58
|
+
// --- name / visibility ---
|
|
59
|
+
get name(): string { return _creator.getName(this.id) }
|
|
60
|
+
set name(value: string) { _creator.setName(this.id, value) }
|
|
61
|
+
get visible(): boolean { return _creator.isVisible(this.id) }
|
|
62
|
+
set visible(value: boolean) { _creator.setVisible(this.id, value) }
|
|
63
|
+
|
|
64
|
+
// --- local transform ---
|
|
65
|
+
// Native owns the authoritative matrix; we keep a Float32Array cache (re-synced lazily because physics
|
|
66
|
+
// can rewrite it each frame). The PUBLIC `matrix` getter hands back a fresh Mat4 *copy* (value
|
|
67
|
+
// semantics — mutating it never touches the node until you assign it back). `_sync()` is the internal
|
|
68
|
+
// hot path that returns the cache directly for cheap component reads.
|
|
69
|
+
private _sync(): Float32Array {
|
|
70
|
+
if (!this._matrix) this._matrix = new Float32Array(16)
|
|
71
|
+
if (this._lastSync === 0 || Date.now() - this._lastSync > 10) {
|
|
72
|
+
_creator.getMatrix(this.id, this._matrix)
|
|
73
|
+
this._scaleCache = undefined
|
|
74
|
+
this._lastSync = Date.now()
|
|
75
|
+
}
|
|
76
|
+
return this._matrix
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get matrix(): Mat4 { return new Mat4(this._sync()) }
|
|
80
|
+
set matrix(m: Mat4Like) {
|
|
81
|
+
if (!this._matrix) this._matrix = new Float32Array(16)
|
|
82
|
+
this._matrix.set(matArr(m))
|
|
83
|
+
this._lastSync = Date.now()
|
|
84
|
+
_creator.setMatrix(this.id, this._matrix)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get worldMatrix(): Mat4 {
|
|
88
|
+
if (this.parent === null) return this.matrix
|
|
89
|
+
if (!this._worldMatrix) this._worldMatrix = new Float32Array(16)
|
|
90
|
+
_creator.getWorldMatrix(this.id, this._worldMatrix)
|
|
91
|
+
return new Mat4(this._worldMatrix)
|
|
92
|
+
}
|
|
93
|
+
set worldMatrix(m: Mat4Like) {
|
|
94
|
+
if (this.parent === null) { this.matrix = m; return }
|
|
95
|
+
this._lastSync = 0
|
|
96
|
+
_creator.setWorldMatrix(this.id, Float32Array.from(matArr(m)))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get position(): Vec3 { const m = this._sync(); return new Vec3(m[12], m[13], m[14]) }
|
|
100
|
+
set position(v: Vec3Like) { this._lastSync = 0; _creator.setPosition(this.id, cx(v), cy(v), cz(v)) }
|
|
101
|
+
|
|
102
|
+
// Scalar position accessors — the loud, correct way to nudge one axis (`node.x = 3`), so nobody reaches
|
|
103
|
+
// for `node.position.x = 3` (a no-op: the getter returns a fresh copy).
|
|
104
|
+
get x(): number { return this._sync()[12] }
|
|
105
|
+
set x(v: number) { const m = this._sync(); this._lastSync = 0; _creator.setPosition(this.id, v, m[13], m[14]) }
|
|
106
|
+
get y(): number { return this._sync()[13] }
|
|
107
|
+
set y(v: number) { const m = this._sync(); this._lastSync = 0; _creator.setPosition(this.id, m[12], v, m[14]) }
|
|
108
|
+
get z(): number { return this._sync()[14] }
|
|
109
|
+
set z(v: number) { const m = this._sync(); this._lastSync = 0; _creator.setPosition(this.id, m[12], m[13], v) }
|
|
110
|
+
|
|
111
|
+
get scale(): Vec3 {
|
|
112
|
+
if (this._lastSync === 0 || !this._scaleCache) {
|
|
113
|
+
const m = this._sync()
|
|
114
|
+
this._scaleCache = [ Math.hypot(m[0], m[1], m[2]), Math.hypot(m[4], m[5], m[6]), Math.hypot(m[8], m[9], m[10]) ]
|
|
115
|
+
}
|
|
116
|
+
return new Vec3(this._scaleCache)
|
|
117
|
+
}
|
|
118
|
+
set scale(v: Vec3Like | number) {
|
|
119
|
+
this._lastSync = 0
|
|
120
|
+
if (typeof v === "number") _creator.setScale(this.id, v, v, v)
|
|
121
|
+
else _creator.setScale(this.id, cx(v), cy(v), cz(v))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get quaternion(): Quat { return new Mat4(this._sync()).rotation }
|
|
125
|
+
set quaternion(v: QuatLike) { this._lastSync = 0; _creator.setQuaternion(this.id, cx(v), cy(v), cz(v), cw(v)) }
|
|
126
|
+
|
|
127
|
+
get eulerAngles(): Vec3 { return new Mat4(this._sync()).eulerAngles }
|
|
128
|
+
set eulerAngles(v: Vec3Like) { this._lastSync = 0; _creator.setEulerAngles(this.id, cx(v), cy(v), cz(v), 0) }
|
|
129
|
+
|
|
130
|
+
// --- world-space reads ---
|
|
131
|
+
get forward(): Vec3 {
|
|
132
|
+
const v = new Float32Array(3)
|
|
133
|
+
_creator.getWorldDirection(this.id, 2, v)
|
|
134
|
+
return new Vec3(-v[0], -v[1], -v[2])
|
|
135
|
+
}
|
|
136
|
+
get worldPosition(): Vec3 {
|
|
137
|
+
const v = new Float32Array(3)
|
|
138
|
+
_creator.getWorldPosition(this.id, v)
|
|
139
|
+
return new Vec3(v[0], v[1], v[2])
|
|
140
|
+
}
|
|
141
|
+
get worldScale(): Vec3 { return this.worldMatrix.scaling }
|
|
142
|
+
get worldQuaternion(): Quat { return this.worldMatrix.rotation }
|
|
143
|
+
get worldEulerAngles(): Vec3 { return this.worldMatrix.eulerAngles }
|
|
144
|
+
|
|
145
|
+
// --- hierarchy ---
|
|
146
|
+
get parent(): Node | null {
|
|
147
|
+
const id = _creator.getParent(this.id)
|
|
148
|
+
return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
|
|
149
|
+
}
|
|
150
|
+
get children(): Node[] {
|
|
151
|
+
return _creator.getChildren(this.id).map((id) => nodeRegistry.get(id) ?? new Node(id))
|
|
152
|
+
}
|
|
153
|
+
get childCount(): number { return _creator.getChildCount(this.id) }
|
|
154
|
+
getChild(index: number): Node | null {
|
|
155
|
+
const id = _creator.getChild(this.id, index)
|
|
156
|
+
return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
|
|
157
|
+
}
|
|
158
|
+
/** Parent the given nodes under this one. */
|
|
159
|
+
add(...children: Node[]): this {
|
|
160
|
+
for (const c of children) _creator.setParent(c.id, this.id, false)
|
|
161
|
+
return this
|
|
162
|
+
}
|
|
163
|
+
setParent(parent: Node | null, worldPositionStays = false): this {
|
|
164
|
+
if (parent === null) _creator.setParentNull(this.id, worldPositionStays)
|
|
165
|
+
else _creator.setParent(this.id, parent.id, worldPositionStays)
|
|
166
|
+
if (worldPositionStays) this._lastSync = 0
|
|
167
|
+
return this
|
|
168
|
+
}
|
|
169
|
+
traverse(callback: (node: Node) => void): void {
|
|
170
|
+
callback(this)
|
|
171
|
+
_creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new Node(id)))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- behaviors are aspects: node.aspect(Physics, …) / node.aspect(Collider, …) ---
|
|
175
|
+
|
|
176
|
+
/** Orient the node so the given axis (default "-z", "forward") points at a world point. */
|
|
177
|
+
lookAt(point: Vec3Like, mode: "z" | "-z" | "x" | "-x" | "y" | "-y" = "-z", ortho: Vec3Like = [ 0, 1, 0 ]): this {
|
|
178
|
+
const world = this.worldMatrix
|
|
179
|
+
const pos = world.position
|
|
180
|
+
let dir = new Vec3(point).sub(pos)
|
|
181
|
+
const len = dir.length()
|
|
182
|
+
if (len === 0) return this
|
|
183
|
+
let axis: string = mode
|
|
184
|
+
if (mode.startsWith("-")) { dir = dir.scale(-1 / len); axis = mode.slice(1) }
|
|
185
|
+
else dir = dir.scale(1 / len)
|
|
186
|
+
|
|
187
|
+
const o = new Vec3(ortho)
|
|
188
|
+
let forward: Vec3, right: Vec3, up: Vec3
|
|
189
|
+
if (axis === "x") {
|
|
190
|
+
right = dir; up = o.cross(right).normalize(); forward = up.cross(right).normalize()
|
|
191
|
+
} else if (axis === "y") {
|
|
192
|
+
up = dir; right = o.cross(up).normalize(); forward = up.cross(right).normalize()
|
|
193
|
+
} else {
|
|
194
|
+
forward = dir; right = o.cross(dir).normalize(); up = dir.cross(right)
|
|
195
|
+
}
|
|
196
|
+
const s = world.scaling, w = world.m
|
|
197
|
+
this.worldMatrix = Float32Array.of(
|
|
198
|
+
right.x * s.x, right.y * s.x, right.z * s.x, 0,
|
|
199
|
+
up.x * s.y, up.y * s.y, up.z * s.y, 0,
|
|
200
|
+
forward.x * s.z, forward.y * s.z, forward.z * s.z, 0,
|
|
201
|
+
w[12], w[13], w[14], w[15],
|
|
202
|
+
)
|
|
203
|
+
return this
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- events ---
|
|
207
|
+
override addEventListener<K extends keyof NodeEvents>(channel: K, callback: NodeEvents[K]): void {
|
|
208
|
+
if (channel === "click") registerTouchEndEvent()
|
|
209
|
+
else if (channel === "touchstart") registerTouchStartEvent()
|
|
210
|
+
super.addEventListener(channel, callback)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @internal — called by the touch system to deliver a hit. */
|
|
214
|
+
_dispatchEvent<K extends keyof NodeEvents>(channel: K, ...args: Parameters<NodeEvents[K]>): void {
|
|
215
|
+
this.dispatch(channel, ...args)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @internal — host animation-complete callback; forwards to the ModelAnimation aspect (if any).
|
|
219
|
+
* Returns whether the clip should keep playing. */
|
|
220
|
+
_onAnimationComplete(clipIndex: number): boolean {
|
|
221
|
+
return this._animComplete?.(clipIndex) ?? true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @internal — emit a GLB animation event (the ModelAnimation aspect can't reach protected dispatch). */
|
|
225
|
+
_emitAnim(channel: "loopReached" | "completed", clip: number): void { this.dispatch(channel, clip) }
|
|
226
|
+
|
|
227
|
+
destroy(): void {
|
|
228
|
+
nodeRegistry.delete(this.id)
|
|
229
|
+
_creator.destroyEntity(this.id)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Read the raw column-major elements out of a Mat4 wrapper or a plain length-16 array. */
|
|
234
|
+
const matArr = (m: Mat4Like): ArrayLike<number> => (m as { m?: ArrayLike<number> }).m ?? (m as ArrayLike<number>)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Native FastNoise (Perlin/fractal) sampler. Ported from worker/src/utils/PerlinNoise.ts.
|
|
2
|
+
|
|
3
|
+
export class Noise {
|
|
4
|
+
private readonly _id: number
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this._id = _creator.createNoise()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Sample 2D noise. */
|
|
11
|
+
get(x: number, y: number): number { return _creator.getNoise2D(this._id, x, y) }
|
|
12
|
+
/** Sample 3D noise. */
|
|
13
|
+
get3d(x: number, y: number, z: number): number { return _creator.getNoise3D(this._id, x, y, z) }
|
|
14
|
+
|
|
15
|
+
private _frequency = 0.01
|
|
16
|
+
get frequency(): number { return this._frequency }
|
|
17
|
+
set frequency(v: number) { this._frequency = v; _creator.setNoiseFrequency(this._id, v) }
|
|
18
|
+
|
|
19
|
+
private _octaves = 1
|
|
20
|
+
get octaves(): number { return this._octaves }
|
|
21
|
+
set octaves(v: number) { this._octaves = v; _creator.setNoiseOctaves(this._id, v) }
|
|
22
|
+
|
|
23
|
+
private _fractalLacunarity = 2
|
|
24
|
+
get fractalLacunarity(): number { return this._fractalLacunarity }
|
|
25
|
+
set fractalLacunarity(v: number) { this._fractalLacunarity = v; _creator.setNoiseFractalLunacrity(this._id, v) }
|
|
26
|
+
|
|
27
|
+
private _fractalGain = 0.5
|
|
28
|
+
get fractalGain(): number { return this._fractalGain }
|
|
29
|
+
set fractalGain(v: number) { this._fractalGain = v; _creator.setNoiseFractalGain(this._id, v) }
|
|
30
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// GPU particle system, as a Node. Created with a material; properties drive the native emitter.
|
|
2
|
+
// Ported from worker/src/components/Particles.ts — folded into the node, so (unlike the worker
|
|
3
|
+
// component) the system exists at construction and setters apply immediately.
|
|
4
|
+
|
|
5
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
6
|
+
import { Material } from "./Material"
|
|
7
|
+
import { Node } from "./Node"
|
|
8
|
+
|
|
9
|
+
type Range<T> = T | { min: T, max: T }
|
|
10
|
+
|
|
11
|
+
type VelocityValue = {
|
|
12
|
+
speed?: Range<number>
|
|
13
|
+
randomizeAngle?: { min: Vec3Like, max: Vec3Like }
|
|
14
|
+
} & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})
|
|
15
|
+
|
|
16
|
+
export type DynamicValue =
|
|
17
|
+
| Range<number>
|
|
18
|
+
| { value?: Range<number>, inc?: Float32Array, inc_b?: Float32Array, mul?: Float32Array, mul_b?: Float32Array }
|
|
19
|
+
|
|
20
|
+
export type DynamicColor =
|
|
21
|
+
| Range<string>
|
|
22
|
+
| { value?: Range<string>, mul: string, mul_b?: string, t: Float32Array }
|
|
23
|
+
|
|
24
|
+
type Shape = { type: "point", v: Vec3Like } | { type: "box", min: Vec3Like, max: Vec3Like }
|
|
25
|
+
type Noise = { strength?: number, frequency?: number, speed?: number }
|
|
26
|
+
|
|
27
|
+
export type ParticlesOptions = {
|
|
28
|
+
material: Material
|
|
29
|
+
rate?: Range<number>
|
|
30
|
+
shape?: Shape
|
|
31
|
+
startVelocity?: Vec3Like | VelocityValue
|
|
32
|
+
gravity?: number
|
|
33
|
+
drag?: number
|
|
34
|
+
lifetime?: Range<number>
|
|
35
|
+
color?: DynamicColor
|
|
36
|
+
size?: DynamicValue
|
|
37
|
+
rotation?: DynamicValue
|
|
38
|
+
noise?: Noise | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Particles extends Node {
|
|
42
|
+
private _material: Material
|
|
43
|
+
|
|
44
|
+
/** Custom curve params (slots 0..3); size/rotation alias slots 0/1. */
|
|
45
|
+
readonly custom: DynamicValue[]
|
|
46
|
+
|
|
47
|
+
constructor(options: ParticlesOptions) {
|
|
48
|
+
super()
|
|
49
|
+
this._material = options.material
|
|
50
|
+
_creator.createParticleSystem(this.id, Material.idOf(options.material))
|
|
51
|
+
|
|
52
|
+
const id = this.id
|
|
53
|
+
this.custom = new Proxy([] as DynamicValue[], {
|
|
54
|
+
set(target, prop: string, value: DynamicValue) {
|
|
55
|
+
const i = parseInt(prop)
|
|
56
|
+
if (Number.isNaN(i)) { (target as any)[prop] = value; return true }
|
|
57
|
+
target[i] = value
|
|
58
|
+
applyParam(id, i, value)
|
|
59
|
+
return true
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (options.rate !== undefined) this.rate = options.rate
|
|
64
|
+
if (options.shape !== undefined) this.shape = options.shape
|
|
65
|
+
if (options.startVelocity !== undefined) this.startVelocity = options.startVelocity
|
|
66
|
+
if (options.gravity !== undefined) this.gravity = options.gravity
|
|
67
|
+
if (options.drag !== undefined) this.drag = options.drag
|
|
68
|
+
if (options.lifetime !== undefined) this.lifetime = options.lifetime
|
|
69
|
+
if (options.color !== undefined) this.color = options.color
|
|
70
|
+
if (options.size !== undefined) this.custom[0] = options.size
|
|
71
|
+
if (options.rotation !== undefined) this.custom[1] = options.rotation
|
|
72
|
+
if (options.noise) this.noise = options.noise
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Emit `count` particles immediately (a burst). */
|
|
76
|
+
spawn(count: number): this { _creator.spawnParticles(this.id, count); return this }
|
|
77
|
+
|
|
78
|
+
set rate(val: Range<number>) {
|
|
79
|
+
if (typeof val === "number") _creator.setParticleSystemRateValue(this.id, val)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
set shape(shape: Shape) {
|
|
83
|
+
if (shape.type === "point") _creator.setParticleSystemShapePoint(this.id, cx(shape.v), cy(shape.v), cz(shape.v))
|
|
84
|
+
else _creator.setParticleSystemShapeBox(this.id, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set startVelocity(val: Vec3Like | VelocityValue) {
|
|
88
|
+
if (Array.isArray(val) || val instanceof Float32Array || val instanceof Vec3) {
|
|
89
|
+
_creator.setParticleSystemVelocityValue(this.id, 1, cx(val), cy(val), cz(val))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
const v = val as VelocityValue
|
|
93
|
+
if ("from" in v) _creator.setParticleSystemVelocityValue(this.id, 2, cx(v.from), cy(v.from), cz(v.from))
|
|
94
|
+
else if ("to" in v) _creator.setParticleSystemVelocityValue(this.id, 3, cx(v.to), cy(v.to), cz(v.to))
|
|
95
|
+
else if ("dir" in v) _creator.setParticleSystemVelocityValue(this.id, 1, cx(v.dir), cy(v.dir), cz(v.dir))
|
|
96
|
+
else _creator.setParticleSystemVelocityValue(this.id, 0, 0, 0, 0)
|
|
97
|
+
if ("speed" in v && v.speed !== undefined) {
|
|
98
|
+
if (typeof v.speed === "number") _creator.setParticleSystemVelocitySpeedValue(this.id, v.speed)
|
|
99
|
+
else _creator.setParticleSystemVelocitySpeedRange(this.id, v.speed.min, v.speed.max)
|
|
100
|
+
}
|
|
101
|
+
if (v.randomizeAngle) {
|
|
102
|
+
const a = v.randomizeAngle
|
|
103
|
+
_creator.setParticleSystemVelocityRndAngle(this.id, cx(a.min), cy(a.min), cz(a.min), cx(a.max), cy(a.max), cz(a.max))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
set gravity(val: number) { _creator.setParticleSystemGravityValue(this.id, val) }
|
|
108
|
+
set drag(val: number) { _creator.setParticleSystemDragValue(this.id, val) }
|
|
109
|
+
|
|
110
|
+
set lifetime(val: Range<number>) {
|
|
111
|
+
if (typeof val === "number") _creator.setParticleSystemLifetimeValue(this.id, val)
|
|
112
|
+
else _creator.setParticleSystemLifetimeRange(this.id, val.min, val.max)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
set size(val: DynamicValue) { this.custom[0] = val }
|
|
116
|
+
set rotation(val: DynamicValue) { this.custom[1] = val }
|
|
117
|
+
|
|
118
|
+
set color(value: DynamicColor) {
|
|
119
|
+
if (typeof value === "string") _creator.setParticleSystemColorValue(this.id, value)
|
|
120
|
+
else if ("min" in value) _creator.setParticleSystemColorRange(this.id, value.min, value.max)
|
|
121
|
+
else {
|
|
122
|
+
if (typeof value.value === "string") _creator.setParticleSystemColorValue(this.id, value.value)
|
|
123
|
+
else if (value.value) _creator.setParticleSystemColorRange(this.id, value.value.min, value.value.max)
|
|
124
|
+
if (value.mul) _creator.setParticleSystemColorCurveA(this.id, value.mul, value.t)
|
|
125
|
+
if (value.mul_b) _creator.setParticleSystemColorCurveB(this.id, value.mul_b, value.t)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get material(): Material { return this._material }
|
|
130
|
+
set material(material: Material) {
|
|
131
|
+
this._material = material
|
|
132
|
+
_creator.setMaterial(this.id, Material.idOf(material), 0)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
set noise(noise: Noise | null) {
|
|
136
|
+
if (noise) _creator.setParticleSystemNoise(this.id, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1)
|
|
137
|
+
else _creator.disableParticleSystemNoise(this.id)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const applyParam = (id: number, i: number, value: DynamicValue): void => {
|
|
142
|
+
if (typeof value === "number") {
|
|
143
|
+
_creator.setParticleSystemParamValue(id, i, value)
|
|
144
|
+
} else if ("min" in value) {
|
|
145
|
+
_creator.setParticleSystemParamRange(id, i, value.min, value.max)
|
|
146
|
+
} else {
|
|
147
|
+
if (typeof value.value === "number") _creator.setParticleSystemParamValue(id, i, value.value)
|
|
148
|
+
else if (value.value) _creator.setParticleSystemParamRange(id, i, value.value.min, value.value.max)
|
|
149
|
+
if (value.inc) _creator.setParticleSystemParamCurveA(id, i, value.inc, 1)
|
|
150
|
+
if (value.inc_b) _creator.setParticleSystemParamCurveB(id, i, value.inc_b)
|
|
151
|
+
if (value.mul) _creator.setParticleSystemParamCurveA(id, i, value.mul, 0)
|
|
152
|
+
if (value.mul_b) _creator.setParticleSystemParamCurveB(id, i, value.mul_b)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- curve builders (unchanged from worker) ---
|
|
157
|
+
|
|
158
|
+
export const dynamicColor = (baseValue: Range<string>, ...args: (number | string | { min: string, max: string })[]): DynamicColor => {
|
|
159
|
+
const t = new Float32Array(args.length / 2)
|
|
160
|
+
const mul: string[] = []
|
|
161
|
+
let mul_b: string[] | undefined
|
|
162
|
+
for (let i = 0; i < args.length / 2; i++) {
|
|
163
|
+
if (typeof args[i * 2] !== "number") throw new Error(`dynamicColor: arg ${i * 2} must be a number`)
|
|
164
|
+
t[i] = args[i * 2] as number
|
|
165
|
+
const val = args[i * 2 + 1]
|
|
166
|
+
if (typeof val === "string") { mul[i] = val; if (mul_b) mul_b[i] = val }
|
|
167
|
+
else if (typeof val === "number") throw new Error(`dynamicColor: arg ${i * 2 + 1} must be a string`)
|
|
168
|
+
else { mul[i] = val.min; if (!mul_b) mul_b = [ ...mul ]; mul_b[i] = val.max }
|
|
169
|
+
}
|
|
170
|
+
return { value: baseValue, mul: mul.join("\0"), mul_b: mul_b?.join("\0"), t }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const dynamic = (baseValue: Range<number>, type: "multiply" | "inc", ...args: (number | { min: number, max: number })[]): DynamicValue => {
|
|
174
|
+
const inc = new Float32Array(args.length)
|
|
175
|
+
let inc_b: Float32Array | undefined
|
|
176
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
177
|
+
if (typeof args[i] !== "number") throw new Error(`dynamic: arg ${i} must be a number`)
|
|
178
|
+
inc[i] = args[i] as number
|
|
179
|
+
if (inc_b) inc_b[i] = args[i] as number
|
|
180
|
+
const val = args[i + 1]
|
|
181
|
+
if (typeof val === "number") { inc[i + 1] = val; if (inc_b) inc_b[i + 1] = val }
|
|
182
|
+
else { inc[i + 1] = val.min; if (!inc_b) inc_b = new Float32Array(inc); inc_b[i + 1] = val.max }
|
|
183
|
+
}
|
|
184
|
+
return type === "inc" ? { value: baseValue, inc, inc_b } : { value: baseValue, mul: inc, mul_b: inc_b }
|
|
185
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Physics body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl.
|
|
2
|
+
// node.aspect(Physics, { motion: 'dynamic' }); node.physics.applyImpulse([0, 6, 0])
|
|
3
|
+
//
|
|
4
|
+
// For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it
|
|
5
|
+
// via velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic
|
|
6
|
+
// bodies are driven with moveTo. (Ported from the former Body class.)
|
|
7
|
+
|
|
8
|
+
import { Aspect } from "../core/Aspect"
|
|
9
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
10
|
+
import type { Node } from "./Node"
|
|
11
|
+
|
|
12
|
+
export type MotionType = "static" | "dynamic" | "kinematic"
|
|
13
|
+
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
|
|
14
|
+
|
|
15
|
+
export type BodyShape =
|
|
16
|
+
| { type: "auto" }
|
|
17
|
+
| { type: "box", halfExtents: Vec3Like }
|
|
18
|
+
| { type: "sphere", radius: number }
|
|
19
|
+
| { type: "cylinder", halfHeight: number, radius: number }
|
|
20
|
+
|
|
21
|
+
export class Physics extends Aspect<"physics", Node> {
|
|
22
|
+
static readonly aspect = "physics"
|
|
23
|
+
|
|
24
|
+
/** Default "dynamic". */
|
|
25
|
+
motion: MotionType = "dynamic"
|
|
26
|
+
/** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
|
|
27
|
+
mass = 1
|
|
28
|
+
/** Default { type: "auto" } — a box from the mesh AABB × world scale. */
|
|
29
|
+
shape: BodyShape = { type: "auto" }
|
|
30
|
+
|
|
31
|
+
private _bodyId = 0
|
|
32
|
+
|
|
33
|
+
onAttach(): void {
|
|
34
|
+
// No physics in this build (CREATOR_GL_PHYSICS=OFF) → bodyId stays 0; all methods no-op.
|
|
35
|
+
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
36
|
+
const motion = MOTION[this.motion]
|
|
37
|
+
const shape = resolveShape(this.node, this.shape)
|
|
38
|
+
if (shape.type === "sphere") {
|
|
39
|
+
this._bodyId = _creator.physicsCreateSphere(this.node.id, shape.radius, motion, this.mass)
|
|
40
|
+
} else if (shape.type === "cylinder") {
|
|
41
|
+
this._bodyId = _creator.physicsCreateCylinder(this.node.id, shape.halfHeight, shape.radius, motion, this.mass)
|
|
42
|
+
} else {
|
|
43
|
+
this._bodyId = _creator.physicsCreateBox(this.node.id, cx(shape.halfExtents), cy(shape.halfExtents), cz(shape.halfExtents), motion, this.mass)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Native Jolt body id. 0 until attached, or if the build has no physics support. */
|
|
48
|
+
get id(): number { return this._bodyId }
|
|
49
|
+
|
|
50
|
+
/** Linear velocity in world units/second (fresh Vec3 on read). */
|
|
51
|
+
get velocity(): Vec3 {
|
|
52
|
+
const out = new Float32Array(3)
|
|
53
|
+
if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
|
|
54
|
+
return new Vec3(out[0], out[1], out[2])
|
|
55
|
+
}
|
|
56
|
+
set velocity(v: Vec3Like) {
|
|
57
|
+
if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Apply an instantaneous impulse (kg·m/s) and wake the body. */
|
|
61
|
+
applyImpulse(v: Vec3Like): this {
|
|
62
|
+
if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
|
|
63
|
+
return this
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Teleport / drive a kinematic (or dynamic) body to a world position. */
|
|
67
|
+
moveTo(p: Vec3Like): this {
|
|
68
|
+
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onDetach(): void {
|
|
73
|
+
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Resolve "auto" to a box from the mesh AABB × world scale (so the physical box matches the rendered
|
|
78
|
+
// size). Falls back to a unit box if there is no geometry.
|
|
79
|
+
const resolveShape = (node: Node, shape: BodyShape): Exclude<BodyShape, { type: "auto" }> => {
|
|
80
|
+
if (shape.type !== "auto") return shape
|
|
81
|
+
const scale = node.worldScale
|
|
82
|
+
let hx = 0.5, hy = 0.5, hz = 0.5
|
|
83
|
+
const geom = node.geometry
|
|
84
|
+
if (geom) {
|
|
85
|
+
const v = geom.vertices
|
|
86
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
|
|
87
|
+
for (let i = 0; i + 2 < v.length; i += 3) {
|
|
88
|
+
if (v[i] < minX) minX = v[i]; if (v[i] > maxX) maxX = v[i]
|
|
89
|
+
if (v[i + 1] < minY) minY = v[i + 1]; if (v[i + 1] > maxY) maxY = v[i + 1]
|
|
90
|
+
if (v[i + 2] < minZ) minZ = v[i + 2]; if (v[i + 2] > maxZ) maxZ = v[i + 2]
|
|
91
|
+
}
|
|
92
|
+
hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
|
|
93
|
+
}
|
|
94
|
+
return { type: "box", halfExtents: [ hx * scale.x, hy * scale.y, hz * scale.z ] }
|
|
95
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A geometric plane (point + normal) with ray intersection. Ported from worker/src/utils/Plane.ts
|
|
2
|
+
// onto the SDK's fluent Vec3 (the mat4 transform() method is omitted — add if needed).
|
|
3
|
+
|
|
4
|
+
import { Vec3, type Vec3Like } from "../math/vec"
|
|
5
|
+
import type { Ray } from "./Ray"
|
|
6
|
+
|
|
7
|
+
export class Plane {
|
|
8
|
+
/** Unit normal. */
|
|
9
|
+
readonly normal: Vec3
|
|
10
|
+
/** Scalar offset: dot(normal, point). */
|
|
11
|
+
readonly d: number
|
|
12
|
+
|
|
13
|
+
constructor(normal: Vec3Like, point: Vec3Like) {
|
|
14
|
+
this.normal = new Vec3(normal).normalize()
|
|
15
|
+
this.d = this.normal.dot(point)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static fromPoints(a: Vec3Like, b: Vec3Like, c: Vec3Like): Plane {
|
|
19
|
+
const A = new Vec3(a)
|
|
20
|
+
return new Plane(new Vec3(b).sub(A).cross(new Vec3(c).sub(A)), a)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static fromCoefficients(a: number, b: number, c: number, d: number): Plane {
|
|
24
|
+
const len = Math.hypot(a, b, c)
|
|
25
|
+
const l2 = len * len
|
|
26
|
+
return new Plane([ a, b, c ], [ (a * d) / l2, (b * d) / l2, (c * d) / l2 ])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Ray-plane parameter t (≥0), or null on a miss (parallel / behind origin). */
|
|
30
|
+
intersectRay(ray: Ray): number | null {
|
|
31
|
+
const denom = this.normal.dot(ray.dir)
|
|
32
|
+
if (Math.abs(denom) < 1e-8) return null
|
|
33
|
+
const t = (this.d - this.normal.dot(ray.origin)) / denom
|
|
34
|
+
return t < 0 ? null : t
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Like intersectRay but allows hits behind the origin (line-plane). */
|
|
38
|
+
intersectLine(ray: Ray): number | null {
|
|
39
|
+
const denom = this.normal.dot(ray.dir)
|
|
40
|
+
if (Math.abs(denom) < 1e-8) return null
|
|
41
|
+
return (this.d - this.normal.dot(ray.origin)) / denom
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Signed distance from a point (positive on the normal's side). */
|
|
45
|
+
signedDistance(point: Vec3Like): number {
|
|
46
|
+
return this.normal.dot(point) - this.d
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Project a point onto the plane. */
|
|
50
|
+
projectPoint(point: Vec3Like): Vec3 {
|
|
51
|
+
return new Vec3(point).scaleAndAdd(this.normal, -this.signedDistance(point))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// A ray defined by an origin and a (not necessarily normalized) direction.
|
|
2
|
+
|
|
3
|
+
import { Vec3, type Vec3Like } from "../math/vec"
|
|
4
|
+
|
|
5
|
+
export class Ray {
|
|
6
|
+
readonly origin: Vec3
|
|
7
|
+
readonly dir: Vec3
|
|
8
|
+
|
|
9
|
+
constructor(origin: Vec3Like, dir: Vec3Like) {
|
|
10
|
+
this.origin = new Vec3(origin)
|
|
11
|
+
this.dir = new Vec3(dir)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The world-space point origin + t·dir. */
|
|
15
|
+
getPoint(t: number): Vec3 { return this.origin.scaleAndAdd(this.dir, t) }
|
|
16
|
+
}
|