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.
Files changed (40) hide show
  1. package/README.md +27 -4
  2. package/dist/index.js +171621 -382
  3. package/package.json +13 -7
  4. package/runtime/sdk/compile/bundler.ts +10 -2
  5. package/runtime/sdk/compile/compileProject.ts +10 -2
  6. package/runtime/sdk/compile/detectEntry.ts +5 -2
  7. package/runtime/sdk/compile/index.ts +17 -1
  8. package/runtime/sdk/compile/serverSplit.ts +233 -0
  9. package/runtime/sdk/compile/serverTypes.ts +227 -0
  10. package/runtime/sdk/gl/AnimationClip.ts +174 -0
  11. package/runtime/sdk/gl/Animator.ts +642 -0
  12. package/runtime/sdk/gl/CharacterController.ts +6 -1
  13. package/runtime/sdk/gl/IK.ts +170 -0
  14. package/runtime/sdk/gl/Material.ts +1 -1
  15. package/runtime/sdk/gl/ModelAnimation.ts +34 -3
  16. package/runtime/sdk/gl/Node.ts +27 -1
  17. package/runtime/sdk/gl/Scene.ts +291 -289
  18. package/runtime/sdk/gl/state.ts +6 -6
  19. package/runtime/sdk/inject.ts +10 -0
  20. package/runtime/sdk/plugins/oauth.ts +61 -0
  21. package/runtime/sdk/runtime/rpc.ts +218 -0
  22. package/runtime/sdk/server/auth/appConfig.ts +12 -0
  23. package/runtime/sdk/server/auth/global.ts +80 -0
  24. package/runtime/sdk/server/auth/host.ts +318 -0
  25. package/runtime/sdk/server/auth/models.ts +83 -0
  26. package/runtime/sdk/server/auth/types.ts +50 -0
  27. package/runtime/sdk/server/channel.ts +56 -0
  28. package/runtime/sdk/server/context.ts +36 -0
  29. package/runtime/sdk/server/db/defineDb.ts +237 -0
  30. package/runtime/sdk/server/db/fields.ts +132 -0
  31. package/runtime/sdk/server/db/httpTransport.ts +93 -0
  32. package/runtime/sdk/server/db/index.ts +7 -0
  33. package/runtime/sdk/server/db/marci/query.ts +412 -0
  34. package/runtime/sdk/server/db/types.ts +202 -0
  35. package/runtime/sdk/server/errors.ts +12 -0
  36. package/runtime/sdk/server/host.ts +74 -0
  37. package/runtime/sdk/server/inject.ts +13 -0
  38. package/runtime/sdk/server/runtime.ts +133 -0
  39. package/runtime/sdk/server/validate.ts +87 -0
  40. 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 = true
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
- get clips(): { name: string, duration: number }[] { return _creator.getGlbAnimations(this.node.id) }
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
  }
@@ -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
- if (this._lastSync === 0 || Date.now() - this._lastSync > 10) {
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