lecodes-cli 0.16.1 → 0.17.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 +27 -4
- package/dist/index.js +171621 -382
- package/package.json +13 -7
- package/runtime/sdk/compile/bundler.ts +10 -2
- package/runtime/sdk/compile/compileProject.ts +10 -2
- package/runtime/sdk/compile/detectEntry.ts +5 -2
- package/runtime/sdk/compile/index.ts +17 -1
- package/runtime/sdk/compile/serverSplit.ts +233 -0
- package/runtime/sdk/compile/serverTypes.ts +227 -0
- package/runtime/sdk/gl/AnimationClip.ts +174 -0
- package/runtime/sdk/gl/Animator.ts +642 -0
- package/runtime/sdk/gl/CharacterController.ts +6 -1
- package/runtime/sdk/gl/IK.ts +170 -0
- package/runtime/sdk/gl/Material.ts +1 -1
- package/runtime/sdk/gl/ModelAnimation.ts +34 -3
- package/runtime/sdk/gl/Node.ts +27 -1
- package/runtime/sdk/gl/Scene.ts +291 -289
- package/runtime/sdk/gl/state.ts +6 -6
- package/runtime/sdk/inject.ts +10 -0
- package/runtime/sdk/plugins/oauth.ts +61 -0
- package/runtime/sdk/runtime/rpc.ts +218 -0
- package/runtime/sdk/server/auth/appConfig.ts +12 -0
- package/runtime/sdk/server/auth/global.ts +80 -0
- package/runtime/sdk/server/auth/host.ts +318 -0
- package/runtime/sdk/server/auth/models.ts +83 -0
- package/runtime/sdk/server/auth/types.ts +50 -0
- package/runtime/sdk/server/channel.ts +56 -0
- package/runtime/sdk/server/context.ts +36 -0
- package/runtime/sdk/server/db/defineDb.ts +237 -0
- package/runtime/sdk/server/db/fields.ts +132 -0
- package/runtime/sdk/server/db/httpTransport.ts +93 -0
- package/runtime/sdk/server/db/index.ts +7 -0
- package/runtime/sdk/server/db/marci/query.ts +412 -0
- package/runtime/sdk/server/db/types.ts +202 -0
- package/runtime/sdk/server/errors.ts +12 -0
- package/runtime/sdk/server/host.ts +74 -0
- package/runtime/sdk/server/inject.ts +13 -0
- package/runtime/sdk/server/runtime.ts +133 -0
- package/runtime/sdk/server/validate.ts +87 -0
- package/runtime/sdk-types.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// IK — precise character actions as late-phase aspects on BONES (docs/animation-plan.md §2.5).
|
|
2
|
+
// They run after the animation wrote the pose (base anim or Animator) and before the skin is
|
|
3
|
+
// flushed, so their bone writes land in the skin. Attach to the END bone of the chain; the chain is
|
|
4
|
+
// walked up through parents — one instance per end effector, any Node hierarchy works.
|
|
5
|
+
//
|
|
6
|
+
// const foot = hero.bone('LeftFoot')!
|
|
7
|
+
// foot.aspect(IK.TwoBone, { target: footTarget, pole: kneeHint }) // upLeg → leg → foot
|
|
8
|
+
// foot.ik.weight = grounded ? 1 : 0 // blend in/out
|
|
9
|
+
// hero.bone('Head')!.aspect(IK.LookAt, { target: camera, limit: 70 }) // head tracks the camera
|
|
10
|
+
//
|
|
11
|
+
// Pure SDK/JS: a handful of world-matrix reads + two quaternion writes per solve. A native solver can
|
|
12
|
+
// slot in later behind the same API.
|
|
13
|
+
|
|
14
|
+
import { Aspect } from "../core/Aspect"
|
|
15
|
+
import { Vec3, type Vec3Like } from "../math/vec"
|
|
16
|
+
import { Quat } from "../math/quat"
|
|
17
|
+
import type { Node } from "./Node"
|
|
18
|
+
|
|
19
|
+
type Target = Node | Vec3Like
|
|
20
|
+
|
|
21
|
+
const targetPos = (t: Target | undefined): Vec3 | null => {
|
|
22
|
+
if (t === undefined || t === null) return null
|
|
23
|
+
const n = t as Node
|
|
24
|
+
if (typeof (n as { worldPosition?: unknown }).worldPosition === "object" && typeof n.id === "number") return n.worldPosition
|
|
25
|
+
return new Vec3(t as Vec3Like)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** World rotation of a node (parent chain composed natively). */
|
|
29
|
+
const worldRot = (n: Node): Quat => n.worldMatrix.rotation
|
|
30
|
+
|
|
31
|
+
/** Write a WORLD rotation to a node by converting through the parent's world rotation. */
|
|
32
|
+
const setWorldRot = (n: Node, q: Quat): void => {
|
|
33
|
+
const p = n.parent
|
|
34
|
+
n.quaternion = p ? worldRot(p).invert().mul(q).normalize() : q.normalize()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const clampAngle = (q: Quat, maxRad: number): Quat => {
|
|
38
|
+
// angle of a unit quaternion = 2·acos(|w|)
|
|
39
|
+
const w = Math.min(1, Math.abs(q.w))
|
|
40
|
+
const ang = 2 * Math.acos(w)
|
|
41
|
+
if (ang <= maxRad || ang < 1e-6) return q
|
|
42
|
+
const s = Math.sqrt(1 - w * w)
|
|
43
|
+
const axis = new Vec3(q.x / s, q.y / s, q.z / s)
|
|
44
|
+
return Quat.fromAxisAngle(axis, q.w < 0 ? -maxRad : maxRad)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })`
|
|
48
|
+
* solves upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend
|
|
49
|
+
* (knee/elbow) — a Node or world point. */
|
|
50
|
+
class TwoBone extends Aspect<"ik", Node> {
|
|
51
|
+
static readonly aspect = "ik"
|
|
52
|
+
|
|
53
|
+
/** Where the end bone should be (Node or world position). */
|
|
54
|
+
target?: Target
|
|
55
|
+
/** Bend hint — the mid joint is pulled toward it (Node or world position). */
|
|
56
|
+
pole?: Target
|
|
57
|
+
/** 0–1 contribution (blend in/out, e.g. foot planting only while grounded). */
|
|
58
|
+
weight = 1
|
|
59
|
+
/** Solve every frame (default). Set false to drive `solve()` yourself. */
|
|
60
|
+
enabled = true
|
|
61
|
+
|
|
62
|
+
update(): void {
|
|
63
|
+
if (this.enabled) this.solve()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One solve at the current pose. Safe to call manually (e.g. from a later-ordered aspect). */
|
|
67
|
+
solve(): void {
|
|
68
|
+
const t = targetPos(this.target)
|
|
69
|
+
if (!t || this.weight <= 0) return
|
|
70
|
+
const end = this.node
|
|
71
|
+
const mid = end.parent
|
|
72
|
+
const root = mid?.parent
|
|
73
|
+
if (!mid || !root) return
|
|
74
|
+
|
|
75
|
+
const w = Math.min(1, this.weight)
|
|
76
|
+
const midLocal0 = mid.quaternion
|
|
77
|
+
const rootLocal0 = root.quaternion
|
|
78
|
+
|
|
79
|
+
const a = root.worldPosition, b = mid.worldPosition, c = end.worldPosition
|
|
80
|
+
const l1 = a.distanceTo(b), l2 = b.distanceTo(c)
|
|
81
|
+
if (l1 < 1e-6 || l2 < 1e-6) return
|
|
82
|
+
const eps = 1e-4
|
|
83
|
+
const toT = t.sub(a)
|
|
84
|
+
const d = Math.min(Math.max(toT.length(), eps), l1 + l2 - eps)
|
|
85
|
+
|
|
86
|
+
// 1. bend the mid joint to the angle the target distance demands (law of cosines)
|
|
87
|
+
const ba = a.sub(b), bc = c.sub(b)
|
|
88
|
+
const cur = ba.angle(bc)
|
|
89
|
+
const want = Math.acos(Math.min(1, Math.max(-1, (l1 * l1 + l2 * l2 - d * d) / (2 * l1 * l2))))
|
|
90
|
+
let axis = bc.cross(ba)
|
|
91
|
+
if (axis.lengthSq() < 1e-10) {
|
|
92
|
+
// straight limb: bend toward the pole (or any perpendicular)
|
|
93
|
+
const p = targetPos(this.pole)
|
|
94
|
+
const ref = p ? p.sub(b) : new Vec3(0, 0, 1)
|
|
95
|
+
axis = ba.cross(ref)
|
|
96
|
+
if (axis.lengthSq() < 1e-10) axis = ba.cross(new Vec3(0, 1, 0))
|
|
97
|
+
if (axis.lengthSq() < 1e-10) axis = ba.cross(new Vec3(1, 0, 0))
|
|
98
|
+
}
|
|
99
|
+
axis = axis.normalize()
|
|
100
|
+
// rotating mid by (want - cur) about `axis` opens/closes the angle between ba and bc
|
|
101
|
+
const midWorld = worldRot(mid)
|
|
102
|
+
setWorldRot(mid, Quat.fromAxisAngle(axis, want - cur).mul(midWorld))
|
|
103
|
+
|
|
104
|
+
// 2. swing the root so the end lands on the target direction
|
|
105
|
+
const c2 = end.worldPosition
|
|
106
|
+
const rootWorld = worldRot(root)
|
|
107
|
+
setWorldRot(root, Quat.fromTo(c2.sub(a), toT).mul(rootWorld))
|
|
108
|
+
|
|
109
|
+
// 3. pole: spin the root about the root→target axis so the mid joint lies toward the pole
|
|
110
|
+
const p = targetPos(this.pole)
|
|
111
|
+
if (p) {
|
|
112
|
+
const dir = toT.normalize()
|
|
113
|
+
const b2 = mid.worldPosition
|
|
114
|
+
const midProj = b2.sub(a).sub(dir.scale(b2.sub(a).dot(dir)))
|
|
115
|
+
const poleProj = p.sub(a).sub(dir.scale(p.sub(a).dot(dir)))
|
|
116
|
+
if (midProj.lengthSq() > 1e-10 && poleProj.lengthSq() > 1e-10) {
|
|
117
|
+
setWorldRot(root, Quat.fromTo(midProj, poleProj).mul(worldRot(root)))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4. weight: blend from the animated pose to the solved one
|
|
122
|
+
if (w < 1) {
|
|
123
|
+
root.quaternion = rootLocal0.slerp(root.quaternion, w)
|
|
124
|
+
mid.quaternion = midLocal0.slerp(mid.quaternion, w)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Aim a bone at a target (head / eyes / turret). Attach to the bone itself:
|
|
130
|
+
* `head.aspect(IK.LookAt, { target: camera, limit: 70 })`. `axis` is the bone's LOCAL forward
|
|
131
|
+
* (the direction that should point at the target) — rigs differ; default −Z, Mixamo heads look
|
|
132
|
+
* along +Z of the head bone in most exports, so pass `axis: [0, 0, 1]` there if it faces backwards. */
|
|
133
|
+
class LookAt extends Aspect<"lookAt", Node> {
|
|
134
|
+
static readonly aspect = "lookAt"
|
|
135
|
+
|
|
136
|
+
target?: Target
|
|
137
|
+
/** The bone's local axis that should point at the target. */
|
|
138
|
+
axis: Vec3Like = [ 0, 0, -1 ]
|
|
139
|
+
/** Max deflection from the animated direction, in degrees (default 80). */
|
|
140
|
+
limit = 80
|
|
141
|
+
/** 0–1 contribution. */
|
|
142
|
+
weight = 1
|
|
143
|
+
enabled = true
|
|
144
|
+
|
|
145
|
+
update(): void {
|
|
146
|
+
if (this.enabled) this.solve()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
solve(): void {
|
|
150
|
+
const t = targetPos(this.target)
|
|
151
|
+
if (!t || this.weight <= 0) return
|
|
152
|
+
const bone = this.node
|
|
153
|
+
const w = Math.min(1, this.weight)
|
|
154
|
+
const local0 = bone.quaternion
|
|
155
|
+
const from = bone.worldPosition
|
|
156
|
+
const dir = t.sub(from)
|
|
157
|
+
if (dir.lengthSq() < 1e-10) return
|
|
158
|
+
const rot = worldRot(bone)
|
|
159
|
+
const current = new Vec3(this.axis).rotate(rot)
|
|
160
|
+
let delta = Quat.fromTo(current, dir)
|
|
161
|
+
delta = clampAngle(delta, (this.limit * Math.PI) / 180)
|
|
162
|
+
setWorldRot(bone, delta.mul(rot))
|
|
163
|
+
if (w < 1) bone.quaternion = local0.slerp(bone.quaternion, w)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Inverse kinematics aspects — attach to bones (see file header). */
|
|
168
|
+
export const IK = { TwoBone, LookAt }
|
|
169
|
+
export type IKTwoBone = TwoBone
|
|
170
|
+
export type IKLookAt = LookAt
|
|
@@ -81,7 +81,7 @@ export class Material {
|
|
|
81
81
|
readonly uniforms: Record<string, UniformValue>
|
|
82
82
|
|
|
83
83
|
private _colorKey: string | null = null
|
|
84
|
-
private _useMapFlag =
|
|
84
|
+
private _useMapFlag = false
|
|
85
85
|
|
|
86
86
|
constructor(shader: FetchResponse | number) {
|
|
87
87
|
if (typeof shader === "number") {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// GLB skeletal animation as an aspect. Auto-attached by Model (a glb always has its clips), so you
|
|
2
2
|
// reach it directly: model.anim.play('Run'). Clips come baked in the glb; play by name or index.
|
|
3
|
+
// This is the BASE player (one clip at a time, Filament's Animator underneath). For crossfades,
|
|
4
|
+
// blend spaces, layers and clips from other files attach the Animator aspect — it takes the skeleton
|
|
5
|
+
// over while attached and this player is suspended (docs/animation-plan.md).
|
|
3
6
|
|
|
4
7
|
import { Aspect } from "../core/Aspect"
|
|
5
8
|
import { nodeRegistry } from "./Node"
|
|
9
|
+
import { AnimationClip } from "./AnimationClip"
|
|
6
10
|
import type { Model } from "./Model"
|
|
7
11
|
|
|
8
12
|
let animatorRegistered = false
|
|
@@ -19,6 +23,8 @@ export class ModelAnimation extends Aspect<"anim", Model> {
|
|
|
19
23
|
private _clip = 0
|
|
20
24
|
private _speed = 1
|
|
21
25
|
private _loop = false
|
|
26
|
+
private _suspended = false
|
|
27
|
+
private _clips?: AnimationClip[]
|
|
22
28
|
|
|
23
29
|
onAttach(): void {
|
|
24
30
|
this.node._animComplete = (clipIndex) => {
|
|
@@ -29,17 +35,41 @@ export class ModelAnimation extends Aspect<"anim", Model> {
|
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
/** The clips baked into the glb
|
|
33
|
-
|
|
38
|
+
/** The clips baked into the glb, as AnimationClip objects (usable in an Animator's clip table —
|
|
39
|
+
* e.g. share one model's baked clip with another model of the same rig). */
|
|
40
|
+
get clips(): AnimationClip[] {
|
|
41
|
+
if (this._clips) return this._clips
|
|
42
|
+
const setId = typeof _creator.getGlbClipSet === "function" ? _creator.getGlbClipSet(this.node.id) : 0
|
|
43
|
+
if (setId) this._clips = AnimationClip._ofSet(setId)
|
|
44
|
+
else {
|
|
45
|
+
// no embedded set (older / lite hosts, or a GLB without animations): name + duration only
|
|
46
|
+
this._clips = ((_creator.getGlbAnimations(this.node.id) ?? []) as { name: string, duration: number }[])
|
|
47
|
+
.map((c) => ({ name: c.name, duration: c.duration, trackCount: 0 }) as unknown as AnimationClip)
|
|
48
|
+
}
|
|
49
|
+
return this._clips
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @internal — an Animator owns the skeleton while attached; this player goes quiet. */
|
|
53
|
+
_suspend(on: boolean): void {
|
|
54
|
+
this._suspended = on
|
|
55
|
+
if (on && this._playing) { this._playing = false }
|
|
56
|
+
}
|
|
57
|
+
private _blocked(): boolean {
|
|
58
|
+
if (!this._suspended) return false
|
|
59
|
+
console.warn("model.anim is suspended while an Animator is attached — use model.animator")
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
34
62
|
|
|
35
63
|
get playing(): boolean { return this._playing }
|
|
36
64
|
set playing(value: boolean) {
|
|
65
|
+
if (this._blocked()) return
|
|
37
66
|
this._playing = value
|
|
38
67
|
_creator.setAnimation(this.node.id, value, this._clip, this._speed)
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
/** Play a clip by name or index. Chainable. */
|
|
42
71
|
play(clip?: string | number, options: { loop?: boolean } = {}): this {
|
|
72
|
+
if (this._blocked()) return this
|
|
43
73
|
if (typeof clip === "number") this._clip = clip
|
|
44
74
|
else if (typeof clip === "string") {
|
|
45
75
|
const i = this.clips.findIndex((c) => c.name === clip)
|
|
@@ -57,8 +87,9 @@ export class ModelAnimation extends Aspect<"anim", Model> {
|
|
|
57
87
|
get speed(): number { return this._speed }
|
|
58
88
|
set speed(v: number) {
|
|
59
89
|
this._speed = v
|
|
90
|
+
if (this._suspended) return
|
|
60
91
|
_creator.setAnimation(this.node.id, this._playing, this._clip, v)
|
|
61
92
|
}
|
|
62
93
|
get time(): number { return _creator.getAnimationTime(this.node.id) }
|
|
63
|
-
set time(v: number) { _creator.setAnimationTime(this.node.id, this._clip, v) }
|
|
94
|
+
set time(v: number) { if (!this._blocked()) _creator.setAnimationTime(this.node.id, this._clip, v) }
|
|
64
95
|
}
|
package/runtime/sdk/gl/Node.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Mat4, type Mat4Like } from "../math/mat4"
|
|
|
13
13
|
import type { Geometry } from "./Geometry"
|
|
14
14
|
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
15
15
|
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
16
|
+
import { glState } from "./state"
|
|
16
17
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
17
18
|
|
|
18
19
|
/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
|
|
@@ -45,8 +46,10 @@ export class Node extends AspectHost<NodeEvents> {
|
|
|
45
46
|
|
|
46
47
|
private _matrix?: Float32Array
|
|
47
48
|
private _lastSync = 0
|
|
49
|
+
private _syncFrame = -1
|
|
48
50
|
private _worldMatrix?: Float32Array
|
|
49
51
|
private _scaleCache?: [number, number, number]
|
|
52
|
+
private _boneCache?: Map<string, Node | null>
|
|
50
53
|
|
|
51
54
|
/** @internal — the active animation aspect's clip-complete handler (installed by ModelAnimation). */
|
|
52
55
|
_animComplete?: (clipIndex: number) => boolean
|
|
@@ -73,10 +76,13 @@ export class Node extends AspectHost<NodeEvents> {
|
|
|
73
76
|
// hot path that returns the cache directly for cheap component reads.
|
|
74
77
|
private _sync(): Float32Array {
|
|
75
78
|
if (!this._matrix) this._matrix = new Float32Array(16)
|
|
76
|
-
|
|
79
|
+
// stale once per render frame (glState.frame — physics/controllers/animation rewrite transforms
|
|
80
|
+
// between frames), or after 10 ms of wall time when no scene loop is running
|
|
81
|
+
if (this._lastSync === 0 || this._syncFrame !== glState.frame || Date.now() - this._lastSync > 10) {
|
|
77
82
|
_creator.getMatrix(this.id, this._matrix)
|
|
78
83
|
this._scaleCache = undefined
|
|
79
84
|
this._lastSync = Date.now()
|
|
85
|
+
this._syncFrame = glState.frame
|
|
80
86
|
}
|
|
81
87
|
return this._matrix
|
|
82
88
|
}
|
|
@@ -178,6 +184,26 @@ export class Node extends AspectHost<NodeEvents> {
|
|
|
178
184
|
callback(this)
|
|
179
185
|
_creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new Node(id)))
|
|
180
186
|
}
|
|
187
|
+
/** Find a descendant by name — bones of a loaded Model included (`hero.bone('RightHand').add(sword)`).
|
|
188
|
+
* Exact name first, then the part after the last `:` / `|` (Mixamo `mixamorig:Hips` matches `Hips`).
|
|
189
|
+
* Cached per name; null if absent. */
|
|
190
|
+
bone(name: string): Node | null {
|
|
191
|
+
const cache = (this._boneCache ??= new Map<string, Node | null>())
|
|
192
|
+
const hit = cache.get(name)
|
|
193
|
+
if (hit !== undefined) return hit
|
|
194
|
+
let exact: Node | null = null
|
|
195
|
+
let loose: Node | null = null
|
|
196
|
+
const want = name.slice(name.search(/[^:|]*$/))
|
|
197
|
+
_creator.traverse(this.id, (id) => {
|
|
198
|
+
if (exact) return
|
|
199
|
+
const n = _creator.getName(id)
|
|
200
|
+
if (n === name) exact = nodeRegistry.get(id) ?? new Node(id)
|
|
201
|
+
else if (!loose && n.slice(n.search(/[^:|]*$/)) === want) loose = nodeRegistry.get(id) ?? new Node(id)
|
|
202
|
+
})
|
|
203
|
+
const found = exact ?? loose
|
|
204
|
+
cache.set(name, found)
|
|
205
|
+
return found
|
|
206
|
+
}
|
|
181
207
|
|
|
182
208
|
// --- behaviors are aspects: node.aspect(Shape, …) / node.aspect(Physics, …) ---
|
|
183
209
|
|