lecodes-sdk 1.0.0 → 1.1.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 (78) hide show
  1. package/dist/global.d.ts +18 -4
  2. package/dist/types/animate/tween/Animation.d.ts +69 -0
  3. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  4. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  5. package/dist/types/animate/tween/easing.d.ts +29 -0
  6. package/dist/types/animate/tween/spec.d.ts +178 -0
  7. package/dist/types/g2/Node2D.d.ts +16 -0
  8. package/dist/types/g2/Sprite.d.ts +11 -1
  9. package/dist/types/gl/Camera.d.ts +15 -1
  10. package/dist/types/gl/Foliage.d.ts +47 -0
  11. package/dist/types/gl/Geometry.d.ts +24 -0
  12. package/dist/types/gl/Light.d.ts +25 -7
  13. package/dist/types/gl/Lightmap.d.ts +90 -60
  14. package/dist/types/gl/Material.d.ts +28 -20
  15. package/dist/types/gl/Model.d.ts +7 -5
  16. package/dist/types/gl/Node.d.ts +18 -0
  17. package/dist/types/gl/Particles.d.ts +40 -1
  18. package/dist/types/gl/Scene.d.ts +20 -0
  19. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  20. package/dist/types/gl/animation/Animator.d.ts +27 -0
  21. package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
  22. package/dist/types/gl/animation/IK.d.ts +86 -30
  23. package/dist/types/gl/animation/Warp.d.ts +2 -1
  24. package/dist/types/gl/animation/core.d.ts +35 -4
  25. package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
  26. package/dist/types/gl/terrain/Terrain.d.ts +4 -2
  27. package/dist/types/inject.d.ts +8 -2
  28. package/dist/types/scene/defineScene.d.ts +44 -32
  29. package/dist/types/ui/UIButton.d.ts +3 -1
  30. package/dist/types/ui/UIInput.d.ts +5 -1
  31. package/dist/types/ui/UINode.d.ts +24 -24
  32. package/dist/types.json +1 -1
  33. package/package.json +1 -1
  34. package/prompts/core-design.md +27 -4
  35. package/prompts/core.md +35 -6
  36. package/prompts/select.ts +19 -4
  37. package/src/animate/tween/Animation.ts +378 -0
  38. package/src/animate/tween/Timeline.ts +175 -0
  39. package/src/animate/tween/animateValue.ts +100 -0
  40. package/src/animate/tween/easing.ts +172 -0
  41. package/src/animate/tween/spec.ts +479 -0
  42. package/src/bridges.d.ts +226 -65
  43. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  44. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  45. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  46. package/src/compile/bundler.ts +34 -4
  47. package/src/compile/compileProject.ts +31 -1
  48. package/src/compile/detectEntry.ts +8 -3
  49. package/src/compile/index.ts +2 -0
  50. package/src/compile/serverSplit.ts +9 -3
  51. package/src/g2/Node2D.ts +38 -0
  52. package/src/g2/Sprite.ts +20 -1
  53. package/src/gl/Camera.ts +34 -1
  54. package/src/gl/Foliage.ts +102 -0
  55. package/src/gl/Geometry.ts +393 -348
  56. package/src/gl/Light.ts +46 -16
  57. package/src/gl/Lightmap.ts +439 -275
  58. package/src/gl/Material.ts +59 -47
  59. package/src/gl/Model.ts +167 -156
  60. package/src/gl/Node.ts +39 -0
  61. package/src/gl/Particles.ts +61 -2
  62. package/src/gl/Scene.ts +34 -1
  63. package/src/gl/animation/AnimationClip.ts +52 -0
  64. package/src/gl/animation/Animator.ts +42 -2
  65. package/src/gl/animation/DynamicBone.ts +482 -459
  66. package/src/gl/animation/IK.ts +173 -152
  67. package/src/gl/animation/Playback.ts +5 -4
  68. package/src/gl/animation/Warp.ts +5 -2
  69. package/src/gl/animation/core.ts +65 -4
  70. package/src/gl/physics/Ragdoll.ts +451 -272
  71. package/src/gl/terrain/Terrain.ts +4 -2
  72. package/src/inject.ts +12 -2
  73. package/src/scene/defineScene.ts +72 -62
  74. package/src/ui/UIButton.ts +2 -2
  75. package/src/ui/UIInput.ts +3 -3
  76. package/src/ui/UINode.ts +61 -36
  77. package/dist/types/animate/animate.d.ts +0 -20
  78. package/src/animate/animate.ts +0 -238
@@ -1,151 +1,173 @@
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.
1
+ // IK — chains the ENGINE solves in its late pass: after every aspect's update(dt) ran, before the
2
+ // dynamic bones and the skin flush. Attach to the END bone of the chain (the chain is its parent and
3
+ // grandparent); nothing runs in JS per frame — the aspect only describes the chain, the host reads the
4
+ // target nodes itself each frame and creator-anim (anchors.cpp) solves.
5
5
  //
6
6
  // const foot = hero.bone('LeftFoot')!
7
7
  // foot.aspect(IK.TwoBone, { target: footTarget, pole: kneeHint }) // upLeg → leg → foot
8
8
  // foot.ik.weight = grounded ? 1 : 0 // blend in/out
9
9
  // hero.bone('Head')!.aspect(IK.LookAt, { target: camera, limit: 70 }) // head tracks the camera
10
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.
11
+ // ANCHORED chains — hands on a weapon's grips (the gun-master rig): the chain rides a SOCKET of the
12
+ // rig (`anim.sockets(...)`: named empties on a bone, a weapon's grip / magazine / bolt), keeping the
13
+ // animated hand's pose RELATIVE to that socket on the gun the take was authored on and replaying it
14
+ // relative to the socket on the gun in the hands — shifted and turned by the sockets' difference,
15
+ // never re-authored. A clip's anchor spans (`clip.anchors(...)`) move the hand between sockets while
16
+ // it plays (grip → magazine → free → bolt → grip through a reload):
17
+ //
18
+ // arms.anim.sockets('ik_hand_gun', { grip_r: weapon.gripR, grip_l: weapon.gripL, mag: weapon.mag })
19
+ // arms.anim.sockets('ik_hand_gun', { grip_r: {...}, grip_l: {...}, mag: {...} }, { set: 'tr15' }) // the donor's
20
+ // arms.bone('hand_l')!.aspect(IK.TwoBone, { anchor: 'grip_l' })
21
+ // arms.anim.clip('R_ReloadEmpty')!.anchors('hand_l', { donor: 'tr15', spans: [[0.9, 1.4, 'mag'], [1.4, 2.0, ''], [2.0, 2.4, 'mag']] })
22
+ //
23
+ // Since the engine solves after the late phase, a target the game moves in its own update(dt) — the
24
+ // gun bone placed for aiming and recoil — is reached the SAME frame.
13
25
 
14
26
  import { Aspect } from "../../core/Aspect"
15
27
  import { Vec3, type Vec3Like } from "../../math/vec"
16
28
  import { Quat } from "../../math/quat"
17
29
  import type { Node } from "../Node"
30
+ import { Animator } from "./Animator"
31
+ import type { Core } from "./core"
18
32
 
19
33
  type Target = Node | Vec3Like
20
34
 
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
- }
35
+ const isNode = (t: unknown): t is Node => typeof t === "object" && t !== null && typeof (t as Node).id === "number" && typeof (t as { worldMatrix?: unknown }).worldMatrix === "object"
27
36
 
28
- /** World rotation of a node (parent chain composed natively). */
29
- const worldRot = (n: Node): Quat => n.worldMatrix.rotation
37
+ // CANIM_IK_* (creator-anim.h): the fixed-order parameter array of a chain.
38
+ const P = { TARGET: 0, POLE: 3, HAS_POLE: 6, WEIGHT: 7, ROT: 8, ROT_WEIGHT: 12, AXIS: 13, LIMIT: 16, ENABLED: 17, ANCHOR_TIME: 18, ANCHOR_PIN: 19, COUNT: 20 }
39
+ const stateBuf = new Float32Array(4)
30
40
 
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()
41
+ /** The rig a bone belongs to: the nearest ancestor carrying an Animator (every Model has one); a bare
42
+ * node tree gets one on its topmost node. */
43
+ const rigOf = (bone: Node): Core => {
44
+ let top: Node = bone
45
+ for (let n: Node | null = bone; n; n = n.parent) {
46
+ const anim = n.get(Animator)
47
+ if (anim) return anim._core
48
+ top = n
49
+ }
50
+ top.aspect(Animator)
51
+ return top.get(Animator)!._core
35
52
  }
36
53
 
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
- }
54
+ /** What both chains share: the native handle, the parameter push, the readback. */
55
+ abstract class Chain<Name extends string> extends Aspect<Name, Node> {
56
+ protected _rig?: Core
57
+ protected _ik = 0
58
+ protected _weight = 1
59
+ protected _enabled = true
60
+ protected abstract readonly kind: 0 | 1
46
61
 
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
- /** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat.
58
- * Unset = the end bone keeps its animated local rotation (a hand on a grip, a foot on a slope
59
- * want it set). Blended by `weight · rotationWeight`. */
60
- rotation?: Node | Quat
61
- /** 0–1 contribution of `rotation` (on top of `weight`). */
62
- rotationWeight = 1
63
62
  /** 0–1 contribution (blend in/out, e.g. foot planting only while grounded). */
64
- weight = 1
65
- /** Solve every frame (default). Set false to drive `solve()` yourself. */
66
- enabled = true
67
-
68
- update(): void {
69
- if (this.enabled) this.solve()
63
+ get weight(): number { return this._weight }
64
+ set weight(v: number) { this._weight = v; this.push() }
65
+ /** Solve every frame (default). */
66
+ get enabled(): boolean { return this._enabled }
67
+ set enabled(v: boolean) { this._enabled = v; this.push() }
68
+ /** After the last solve: how far the end bone still is from its target, metres (0 = reached). */
69
+ get error(): number { return this._rig && this._ik && this._rig.ikState(this._ik, stateBuf) ? stateBuf[0]! : 0 }
70
+
71
+ onAttach(): void {
72
+ this._rig = rigOf(this.node)
73
+ this._ik = this._rig.ikCreate(this.kind, this.node.id)
74
+ if (!this._ik) console.warn(`IK: ${this.node.name || "the bone"} is no joint of its rig (or this host has no IK)`)
75
+ this.push()
70
76
  }
77
+ onDetach(): void {
78
+ if (this._rig && this._ik) this._rig.ikDestroy(this._ik)
79
+ this._ik = 0
80
+ this._rig = undefined
81
+ }
82
+ onReconfigure(): void { this.push() }
83
+
84
+ protected params(): Float32Array {
85
+ const p = new Float32Array(P.COUNT)
86
+ p[P.WEIGHT] = this._weight
87
+ p[P.ENABLED] = this._enabled ? 1 : 0
88
+ p[P.LIMIT] = 80
89
+ p[P.AXIS + 2] = -1
90
+ p[P.ROT + 3] = 1
91
+ p[P.ANCHOR_TIME] = 0.05
92
+ return p
93
+ }
94
+ /** Push the description to the engine (a no-op until attached). */
95
+ protected abstract push(): void
96
+ }
71
97
 
72
- /** One solve at the current pose. Safe to call manually (e.g. from a later-ordered aspect). */
73
- solve(): void {
74
- const t = targetPos(this.target)
75
- if (!t || this.weight <= 0) return
76
- const end = this.node
77
- const mid = end.parent
78
- const root = mid?.parent
79
- if (!mid || !root) return
80
-
81
- const w = Math.min(1, this.weight)
82
- const midLocal0 = mid.quaternion
83
- const rootLocal0 = root.quaternion
84
-
85
- const a = root.worldPosition, b = mid.worldPosition, c = end.worldPosition
86
- const l1 = a.distanceTo(b), l2 = b.distanceTo(c)
87
- if (l1 < 1e-6 || l2 < 1e-6) return
88
- const eps = 1e-4
89
- const toT = t.sub(a)
90
- const d = Math.min(Math.max(toT.length(), eps), l1 + l2 - eps)
91
-
92
- // 1. bend the mid joint to the angle the target distance demands (law of cosines)
93
- const ba = a.sub(b), bc = c.sub(b)
94
- const cur = ba.angle(bc)
95
- const want = Math.acos(Math.min(1, Math.max(-1, (l1 * l1 + l2 * l2 - d * d) / (2 * l1 * l2))))
96
- // a positive rotation about ba × bc moves `bc` AWAY from `ba`, i.e. opens the joint — so rotating the
97
- // mid bone by (want − cur) about it opens/closes exactly as needed. (bc × ba has the opposite sense:
98
- // with it every already-bent limb — which is every animated limb — bends the wrong way; the straight
99
- // case below picks its axis as ba × ref, the same sense as ba × bc for a limb barely bent toward ref.)
100
- let axis = ba.cross(bc)
101
- if (axis.lengthSq() < 1e-10) {
102
- // straight limb: bend toward the pole (or any perpendicular)
103
- const p = targetPos(this.pole)
104
- const ref = p ? p.sub(b) : new Vec3(0, 0, 1)
105
- axis = ba.cross(ref)
106
- if (axis.lengthSq() < 1e-10) axis = ba.cross(new Vec3(0, 1, 0))
107
- if (axis.lengthSq() < 1e-10) axis = ba.cross(new Vec3(1, 0, 0))
108
- }
109
- axis = axis.normalize()
110
- // rotating mid by (want - cur) about `axis` opens/closes the angle between ba and bc
111
- const midWorld = worldRot(mid)
112
- setWorldRot(mid, Quat.fromAxisAngle(axis, want - cur).mul(midWorld))
113
-
114
- // 2. swing the root so the end lands on the target direction
115
- const c2 = end.worldPosition
116
- const rootWorld = worldRot(root)
117
- setWorldRot(root, Quat.fromTo(c2.sub(a), toT).mul(rootWorld))
118
-
119
- // 3. pole: spin the root about the root→target axis so the mid joint lies toward the pole
120
- const p = targetPos(this.pole)
121
- if (p) {
122
- const dir = toT.normalize()
123
- const b2 = mid.worldPosition
124
- const midProj = b2.sub(a).sub(dir.scale(b2.sub(a).dot(dir)))
125
- const poleProj = p.sub(a).sub(dir.scale(p.sub(a).dot(dir)))
126
- if (midProj.lengthSq() > 1e-10 && poleProj.lengthSq() > 1e-10) {
127
- setWorldRot(root, Quat.fromTo(midProj, poleProj).mul(worldRot(root)))
128
- }
129
- }
130
-
131
- // 4. weight: blend from the animated pose to the solved one
132
- if (w < 1) {
133
- root.quaternion = rootLocal0.slerp(root.quaternion, w)
134
- mid.quaternion = midLocal0.slerp(mid.quaternion, w)
135
- }
98
+ /** Two-bone analytic IK (limbs). Attach to the END bone: `foot.aspect(IK.TwoBone, { target })` solves
99
+ * upper (grandparent) + mid (parent) so the end reaches `target`; `pole` steers the bend (knee /
100
+ * elbow) — a Node or world point; `anchor` makes it ride a socket of the rig instead (see the file
101
+ * header). */
102
+ class TwoBone extends Chain<"ik"> {
103
+ static readonly aspect = "ik"
104
+ protected readonly kind = 0 as const
105
+ private _target?: Target
106
+ private _pole?: Target
107
+ private _rotation?: Node | Quat
108
+ private _rotationWeight = 1
109
+ private _anchor = ""
110
+ private _anchorTime = 0.05
111
+ private _anchorPin = 0
112
+
113
+ /** Where the end bone should be (a Node — read by the engine every frame — or a world position). */
114
+ get target(): Target | undefined { return this._target }
115
+ set target(v: Target | undefined) { this._target = v; this.push() }
116
+ /** Bend hint — the mid joint is pulled toward it (a Node or a world point). */
117
+ get pole(): Target | undefined { return this._pole }
118
+ set pole(v: Target | undefined) { this._pole = v; this.push() }
119
+ /** World rotation the END bone takes after the solve — a Node (its world rotation) or a Quat. Unset =
120
+ * the end bone keeps its animated rotation (a hand on a grip, a foot on a slope want it set). Blended
121
+ * by `weight · rotationWeight`. An anchored chain takes its rotation from the socket instead. */
122
+ get rotation(): Node | Quat | undefined { return this._rotation }
123
+ set rotation(v: Node | Quat | undefined) { this._rotation = v; this.push() }
124
+ /** 0–1 contribution of `rotation` (on top of `weight`); for an anchored chain, of the socket's turn. */
125
+ get rotationWeight(): number { return this._rotationWeight }
126
+ set rotationWeight(v: number) { this._rotationWeight = v; this.push() }
127
+ /** ANCHORED: the name of the LIVE socket the end bone rides (`anim.sockets(...)`) — its target and
128
+ * rotation come from the socket and the playing clips' anchor spans; `target` / `pole` / `rotation`
129
+ * are ignored. '' = a plain chain. */
130
+ get anchor(): string { return this._anchor }
131
+ set anchor(v: string) { this._anchor = v ?? ""; this.push() }
132
+ /** Anchored: how fast the hand moves between sockets — the halflife (s) of the spring the applied
133
+ * delta follows the wanted one with (default 0.05). */
134
+ get anchorTime(): number { return this._anchorTime }
135
+ set anchorTime(v: number) { this._anchorTime = v; this.push() }
136
+ /** Anchored: 0..1 how hard the end bone is PINNED on its socket while the playing clips leave it there
137
+ * (a GAP in their spans — the hand is holding the gun). 1 = it sits exactly on the socket, so nothing
138
+ * in the pose below can slide it along the gun; 0 (default) = it keeps the take's own relation to the
139
+ * socket, moved by the delta. A clip's SPANS carry their own pin and default to 0, because a hand on
140
+ * its way to a magazine must keep the take's motion — so this knob is about holding, not reaching. */
141
+ get anchorPin(): number { return this._anchorPin }
142
+ set anchorPin(v: number) { this._anchorPin = v; this.push() }
143
+ /** After the last solve: the anchor delta applied — how far (m) and how much (rad) the live socket
144
+ * moved the hand off the take's own pose. 0 for a plain chain. */
145
+ get anchorDelta(): { distance: number, angle: number } {
146
+ if (!this._rig || !this._ik || !this._rig.ikState(this._ik, stateBuf)) return { distance: 0, angle: 0 }
147
+ return { distance: stateBuf[1]!, angle: stateBuf[2]! }
148
+ }
136
149
 
137
- // 5. end-bone rotation: take the target's world rotation (a hand stays on its grip, a foot
138
- // flat on the slope) — the solve above only placed the end's ORIGIN
139
- const r = this.rotation
140
- if (r !== undefined && r !== null) {
141
- const rw = w * Math.min(1, Math.max(0, this.rotationWeight))
142
- if (rw > 0) {
143
- const want = (r as Node).id !== undefined && typeof (r as Node).worldMatrix === "object" ? worldRot(r as Node) : (r as Quat)
144
- const endLocal0 = end.quaternion
145
- setWorldRot(end, want)
146
- if (rw < 1) end.quaternion = endLocal0.slerp(end.quaternion, rw)
147
- }
150
+ protected push(): void {
151
+ const rig = this._rig
152
+ if (!rig || !this._ik) return
153
+ const p = this.params()
154
+ p[P.ANCHOR_TIME] = this._anchorTime
155
+ p[P.ANCHOR_PIN] = this._anchorPin
156
+ const anchored = this._anchor !== ""
157
+ p[P.ROT_WEIGHT] = anchored || this._rotation !== undefined ? this._rotationWeight : 0
158
+ let targetNode = 0, poleNode = 0, rotNode = 0
159
+ if (!anchored) {
160
+ const t = this._target, pl = this._pole, r = this._rotation
161
+ if (isNode(t)) targetNode = t.id
162
+ else if (t !== undefined) { const v = new Vec3(t); p[P.TARGET] = v.x; p[P.TARGET + 1] = v.y; p[P.TARGET + 2] = v.z }
163
+ if (isNode(pl)) poleNode = pl.id
164
+ else if (pl !== undefined) { const v = new Vec3(pl); p[P.POLE] = v.x; p[P.POLE + 1] = v.y; p[P.POLE + 2] = v.z; p[P.HAS_POLE] = 1 }
165
+ if (isNode(r)) rotNode = r.id
166
+ else if (r !== undefined) { p[P.ROT] = r.x; p[P.ROT + 1] = r.y; p[P.ROT + 2] = r.z; p[P.ROT + 3] = r.w }
148
167
  }
168
+ rig.ikSet(this._ik, p)
169
+ rig.ikNodes(this._ik, targetNode, poleNode, rotNode)
170
+ rig.ikAnchor(this._ik, this._anchor)
149
171
  }
150
172
  }
151
173
 
@@ -153,37 +175,36 @@ class TwoBone extends Aspect<"ik", Node> {
153
175
  * `head.aspect(IK.LookAt, { target: camera, limit: 70 })`. `axis` is the bone's LOCAL forward
154
176
  * (the direction that should point at the target) — rigs differ; default −Z, Mixamo heads look
155
177
  * along +Z of the head bone in most exports, so pass `axis: [0, 0, 1]` there if it faces backwards. */
156
- class LookAt extends Aspect<"lookAt", Node> {
178
+ class LookAt extends Chain<"lookAt"> {
157
179
  static readonly aspect = "lookAt"
158
-
159
- target?: Target
180
+ protected readonly kind = 1 as const
181
+ private _target?: Target
182
+ private _axis: Vec3Like = [ 0, 0, -1 ]
183
+ private _limit = 80
184
+
185
+ /** What to look at (a Node — read by the engine every frame — or a world position). */
186
+ get target(): Target | undefined { return this._target }
187
+ set target(v: Target | undefined) { this._target = v; this.push() }
160
188
  /** The bone's local axis that should point at the target. */
161
- axis: Vec3Like = [ 0, 0, -1 ]
189
+ get axis(): Vec3Like { return this._axis }
190
+ set axis(v: Vec3Like) { this._axis = v; this.push() }
162
191
  /** Max deflection from the animated direction, in degrees (default 80). */
163
- limit = 80
164
- /** 0–1 contribution. */
165
- weight = 1
166
- enabled = true
167
-
168
- update(): void {
169
- if (this.enabled) this.solve()
170
- }
171
-
172
- solve(): void {
173
- const t = targetPos(this.target)
174
- if (!t || this.weight <= 0) return
175
- const bone = this.node
176
- const w = Math.min(1, this.weight)
177
- const local0 = bone.quaternion
178
- const from = bone.worldPosition
179
- const dir = t.sub(from)
180
- if (dir.lengthSq() < 1e-10) return
181
- const rot = worldRot(bone)
182
- const current = new Vec3(this.axis).rotate(rot)
183
- let delta = Quat.fromTo(current, dir)
184
- delta = clampAngle(delta, (this.limit * Math.PI) / 180)
185
- setWorldRot(bone, delta.mul(rot))
186
- if (w < 1) bone.quaternion = local0.slerp(bone.quaternion, w)
192
+ get limit(): number { return this._limit }
193
+ set limit(v: number) { this._limit = v; this.push() }
194
+
195
+ protected push(): void {
196
+ const rig = this._rig
197
+ if (!rig || !this._ik) return
198
+ const p = this.params()
199
+ const a = new Vec3(this._axis)
200
+ p[P.AXIS] = a.x; p[P.AXIS + 1] = a.y; p[P.AXIS + 2] = a.z
201
+ p[P.LIMIT] = this._limit
202
+ let targetNode = 0
203
+ const t = this._target
204
+ if (isNode(t)) targetNode = t.id
205
+ else if (t !== undefined) { const v = new Vec3(t); p[P.TARGET] = v.x; p[P.TARGET + 1] = v.y; p[P.TARGET + 2] = v.z }
206
+ rig.ikSet(this._ik, p)
207
+ rig.ikNodes(this._ik, targetNode, 0, 0)
187
208
  }
188
209
  }
189
210
 
@@ -1,8 +1,9 @@
1
1
  // Playback — the handle `play()` returns (docs/animator-plan.md). Thenable: `await anim.play('slash')`
2
- // resolves at the clip's HAND-OVER — where its fade-back starts (`end − fadeOut`; the end when there
3
- // is no fadeOut) — with `true`; `false` when it was cut short (replaced / stopped). What
4
- // you call right after the await is what the clip fades into: `anim.play(next)` (a chained one-shot,
5
- // crossfaded), `anim.playLoop(next)` — nothing = back to the loop (or hold, on a layer without one).
2
+ // resolves at the clip's HAND-OVER — its end (the `end` of its window) — with `true`; `false` when it
3
+ // was cut short (replaced / stopped). What you call right after the await is what its last pose
4
+ // transitions into: `anim.play(next)` (a chained one-shot), `anim.playLoop(next)` — nothing = back to
5
+ // the loop over `fadeOut` (or hold, on a layer without one). A cancel window is game logic (a clip
6
+ // event, `progress`, `busy`), not a fade: play the next clip whenever the game says so.
6
7
  // Returning one from an `async` function flattens it into `Promise<boolean>` — return `p.done` (or
7
8
  // make the function sync) when the caller needs the handle.
8
9
 
@@ -2,7 +2,9 @@
2
2
  // before the feet (docs/animation-v2-plan.md §2.7). `stride` scales each leg's step to the speed the body
3
3
  // really travels at (a walk played at 1.9 m/s stops skating); `orientation` turns the lower body toward
4
4
  // where the body really goes while the spine counter-turns (arcs and strafing without a clip per angle).
5
- // Both need the body's motion, which a `Locomotion` feeds every frame; without one leave it off. Off by
5
+ // Both need the body's motion — a `Locomotion` feeds it every frame, or `anim.motion = velocity` when the
6
+ // gait is driven by hand; without either leave it off — and the CLIP's motion: its root motion, or for an
7
+ // in-place loop the pace its pack declared (`lecodes.velocity` in the GLB animation's extras). Off by
6
8
  // default. `step` is the STEP WARP: stride / lift / pitch / slope dials solved in the knee hinge plane.
7
9
  //
8
10
  // model.anim.warp.set(true) // both, with the defaults
@@ -16,7 +18,8 @@ import type { Core } from "./core"
16
18
  export type WarpOptions = {
17
19
  /** Fit the stride to the speed the body actually travels at. `[min, max]` clamps the scale (default
18
20
  * 0.85…1.2). A CORRECTION: a pack whose takes already read right at the speeds it is played at wants
19
- * none of this; open the range for a pack that must cover speeds it was never recorded at. */
21
+ * none of this; open the range for a pack that must cover speeds it was never recorded at. The clip's
22
+ * own speed is its root motion — an in-place loop takes part only with a declared pace (see above). */
20
23
  stride?: boolean | [number, number]
21
24
  /** Turn the lower body toward where the body really travels; a number caps the turn in degrees
22
25
  * (default 20). The whole twist lives in one joint: a few degrees read as a lean, a lot as a broken
@@ -25,15 +25,24 @@ export type PlayOptions = {
25
25
  fade?: number
26
26
  /** Transition seconds for the way in only (overrides `fade`). */
27
27
  fadeIn?: number
28
- /** One-shots: the transition BACK to the loop at the end (overrides `fade`; `0` = cut at the end).
29
- * It starts this long before the clip ends — while it still plays — so the hand-over lands on the
30
- * last pose, not after it. On a layer with no loop the clip HOLDS its last frame (a rock that broke
31
- * stays broken) — `anim.stop({ fade })` is the way back to rest. */
28
+ /** One-shots: the transition BACK to the loop after the end (overrides `fade`; `0` = cut at the end).
29
+ * The clip plays to its last frame; the loop then takes over and the difference between the two poses
30
+ * (and their velocities) decays over this time, so no part of the clip is cut — to play less of it,
31
+ * give it an `end`. On a layer with no loop the clip HOLDS its last frame (a rock that broke stays
32
+ * broken) — `anim.stop({ fade })` is the way back to rest. */
32
33
  fadeOut?: number
33
34
  /** Playback rate for this clip (default 1). */
34
35
  speed?: number
35
36
  /** Rewind even if the clip is already playing (default: rewind only if it isn't). */
36
37
  restart?: boolean
38
+ /** Play a WINDOW of the clip, seconds of the clip: enter at `start` (default 0; only when the play
39
+ * rewinds — see `restart`) and treat `end` (default the clip's end) as the end — the hand-over fires
40
+ * there, the last frame held there, events beyond it never fire. Only the swing of a longer take, the
41
+ * wind-up without the recovery. A window that lives in the clip table belongs in
42
+ * `AnimationClip.from(clip, { from, to })` instead. Not combined with `phase`. Hosts without the
43
+ * window play the whole clip. */
44
+ start?: number
45
+ end?: number
37
46
  /** Enter the clip at a point in the GAIT CYCLE instead of at its start: `'match'` = the phase the
38
47
  * layer shows right now (the walk's left foot is down → the turn clip starts where its left foot
39
48
  * is down too, so nothing slides), or a number 0–1. Clips without a gait cycle ignore it. */
@@ -293,6 +302,12 @@ export class Core {
293
302
  ensure(): boolean {
294
303
  if (this.id) return true
295
304
  if (this.embedded().length === 0 && this.table.size === 0) { console.warn(`Animator: no clips on ${this.node.name || "the node"} — nothing to play`); return false }
305
+ return this.ensureRig()
306
+ }
307
+ /** The native animator for the RIG alone — an IK chain or a socket on a model that has no clips
308
+ * still needs the joint tree described to the engine. False = no transform hierarchy (warned). */
309
+ ensureRig(): boolean {
310
+ if (this.id) return true
296
311
  ensureEvents()
297
312
  this.id = _creator.animatorCreate(this.node.id)
298
313
  if (!this.id) { console.warn("Animator: node has no transform hierarchy"); return false }
@@ -303,6 +318,7 @@ export class Core {
303
318
  if (this._feetParams && _creator.animatorSetFeetParams) _creator.animatorSetFeetParams(this.id, this._feetParams)
304
319
  if (this.speed !== 1) _creator.animatorSetGlobal(this.id, this.speed, false)
305
320
  for (const L of this.layers) if (L.index > 0) this.pushLayer(L)
321
+ for (const s of this._sockets.values()) this.pushSocket(s)
306
322
  return true
307
323
  }
308
324
  destroy(): void {
@@ -378,6 +394,11 @@ export class Core {
378
394
  s.playback = rec
379
395
  _creator.animatorPlay(this.id, s.slot, false, o.speed ?? 1, fadeIn, fadeOut, o.restart ?? false)
380
396
  if (o.turn !== undefined) _creator.animatorSlotSetTurn?.(this.id, s.slot, o.turn * Math.PI / 180, true)
397
+ if (o.start !== undefined || o.end !== undefined) {
398
+ const st = Math.max(0, o.start ?? 0), en = o.end === undefined ? -1 : Math.min(o.end, clip.duration)
399
+ if (en >= 0 && en <= st) console.warn(`Animator: '${name}' window ${st}–${o.end} is empty (${clip.duration.toFixed(2)}s clip)`)
400
+ _creator.animatorSlotSetWindow?.(this.id, s.slot, st, en)
401
+ }
381
402
  if (phase !== undefined) _creator.animatorSeekPhase(this.id, s.slot, phase)
382
403
  return new Playback(rec, this)
383
404
  }
@@ -671,6 +692,11 @@ export class Core {
671
692
  this._warp = params
672
693
  if (this.id) _creator.animatorSetWarpParams(this.id, params)
673
694
  }
695
+ /** The body's world velocity this frame (m/s) — the game side of the stride / orientation fit. Nothing
696
+ * to feed before the animator exists; hosts without the warp stage have no method. */
697
+ setMotion(vx: number, vy: number, vz: number): void {
698
+ if (this.id && _creator.animatorSetMotion) _creator.animatorSetMotion(this.id, vx, vy, vz)
699
+ }
674
700
  /** The feet stage (lock / ground IK), as the engine's fixed-order parameter array. */
675
701
  private _feetParams?: Float32Array
676
702
  setFeetParams(params: Float32Array): void {
@@ -682,6 +708,41 @@ export class Core {
682
708
  footState(side: 0 | 1, out: Float32Array): boolean {
683
709
  return this.id !== 0 && _creator.animatorFootState !== undefined && _creator.animatorFootState(this.id, side, out)
684
710
  }
711
+ // ---- IK chains + sockets (creator-anim anchors.cpp; the aspects in IK.ts) ----
712
+ // Hosts without the chains (an engine predating them) get a warning once and inert chains.
713
+ private static _warnedIk = false
714
+ private hasIk(): boolean {
715
+ if (_creator.animatorIkCreate) return true
716
+ if (!Core._warnedIk) { Core._warnedIk = true; console.warn("IK: this host has no IK chains (engine predates them) — chains and sockets are inert") }
717
+ return false
718
+ }
719
+ /** A chain ending in `endEntity` (kind 0 two-bone: the bone, its parent, its grandparent; 1 look-at). 0 = none. */
720
+ ikCreate(kind: 0 | 1, endEntity: number): number {
721
+ if (!this.ensureRig() || !this.hasIk()) return 0
722
+ return _creator.animatorIkCreate!(this.id, kind, endEntity)
723
+ }
724
+ ikDestroy(ik: number): void { if (this.id && _creator.animatorIkDestroy) _creator.animatorIkDestroy(this.id, ik) }
725
+ ikSet(ik: number, params: Float32Array): void { if (this.id && _creator.animatorIkSet) _creator.animatorIkSet(this.id, ik, params) }
726
+ ikNodes(ik: number, target: number, pole: number, rotation: number): void { if (this.id && _creator.animatorIkNodes) _creator.animatorIkNodes(this.id, ik, target, pole, rotation) }
727
+ ikAnchor(ik: number, socket: string): void { if (this.id && _creator.animatorIkAnchor) _creator.animatorIkAnchor(this.id, ik, socket) }
728
+ ikState(ik: number, out: Float32Array): boolean { return this.id !== 0 && _creator.animatorIkState !== undefined && _creator.animatorIkState(this.id, ik, out) }
729
+ /** Sockets by "set/name", replayed when the animator is (re)created. */
730
+ private readonly _sockets = new Map<string, { set: string, name: string, joint: number, node: number, trs: Float32Array | null }>()
731
+ /** Define a socket on joint `joint` (a bone's entity): a NODE socket (read by the engine every frame) or a
732
+ * fixed TRS in the joint's space. set "" = the live set, any other name = a donor's. */
733
+ setSocket(set: string, name: string, joint: number, node: number, trs: Float32Array | null): void {
734
+ const rec = { set, name, joint, node, trs }
735
+ this._sockets.set(`${set}/${name}`, rec)
736
+ if (this.ensureRig()) this.pushSocket(rec)
737
+ }
738
+ removeSocket(set: string, name: string): void {
739
+ this._sockets.delete(`${set}/${name}`)
740
+ if (this.id && _creator.animatorSetSocket) _creator.animatorSetSocket(this.id, set, name, 0, 0, null)
741
+ }
742
+ private pushSocket(s: { set: string, name: string, joint: number, node: number, trs: Float32Array | null }): void {
743
+ if (this.hasIk() && _creator.animatorSetSocket) _creator.animatorSetSocket(this.id, s.set, s.name, s.joint, s.node, s.trs)
744
+ }
745
+
685
746
  private readonly _steps = new Set<StepHandler>()
686
747
  onStepHandler(cb: StepHandler): void { this._steps.add(cb) }
687
748
  offStepHandler(cb: StepHandler): void { this._steps.delete(cb) }