lecodes-gl-lite 2.0.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 (46) hide show
  1. package/README.md +27 -0
  2. package/package.json +33 -0
  3. package/src/assets/decoders/README.md +13 -0
  4. package/src/assets/decoders/basis_transcoder.js +19 -0
  5. package/src/assets/decoders/basis_transcoder.wasm +0 -0
  6. package/src/assets/decoders/draco_decoder.wasm +0 -0
  7. package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
  8. package/src/assets/neutral_ibl128.ktx +0 -0
  9. package/src/core/animBridge.ts +51 -0
  10. package/src/core/boneNames.ts +22 -0
  11. package/src/core/frame.ts +230 -0
  12. package/src/core/glbStructure.ts +100 -0
  13. package/src/core/headlessBridge.ts +92 -0
  14. package/src/core/mapView.ts +195 -0
  15. package/src/core/math3d.ts +275 -0
  16. package/src/core/particleBridge.ts +154 -0
  17. package/src/core/physicsBridge.ts +305 -0
  18. package/src/core/recording.ts +59 -0
  19. package/src/core/sceneCore.ts +833 -0
  20. package/src/core/serialize.ts +144 -0
  21. package/src/gen/unsupported.gen.ts +212 -0
  22. package/src/index.ts +5 -0
  23. package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
  24. package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
  25. package/src/vendor/creator-anim/js/index.ts +656 -0
  26. package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
  27. package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
  28. package/src/vendor/creator-nav/js/index.ts +400 -0
  29. package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
  30. package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
  31. package/src/vendor/creator-particles/js/index.ts +137 -0
  32. package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
  33. package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
  34. package/src/vendor/creator-physics/js/index.ts +515 -0
  35. package/src/vite-env.d.ts +1 -0
  36. package/src/webgl/batching.ts +453 -0
  37. package/src/webgl/createViewerLite.ts +1627 -0
  38. package/src/webgl/customMaterial.ts +228 -0
  39. package/src/webgl/decoders/basis.ts +123 -0
  40. package/src/webgl/decoders/draco.ts +110 -0
  41. package/src/webgl/decoders/index.ts +68 -0
  42. package/src/webgl/decoders/worker.ts +45 -0
  43. package/src/webgl/editor.ts +1297 -0
  44. package/src/webgl/glb.ts +593 -0
  45. package/src/webgl/ibl.ts +137 -0
  46. package/src/webgl/shaders.ts +779 -0
@@ -0,0 +1,656 @@
1
+ // creator-anim's TypeScript side over the wasm build (dist/creatorAnim.js): the animation bridge the
2
+ // WEB hosts (viewer-lite's WebGL engine, lecodes-headless headless) hand to the app — the same
3
+ // `_creator` animation method shapes creator-gl exposes natively, so the SDK never knows which host
4
+ // it runs on, and the SAME evaluator (one C++ implementation, no TS twin).
5
+ //
6
+ // The host supplies its scene graph (`AnimHost`): entity lookup with a local matrix per node, a
7
+ // "local matrix changed" notification, the fetch store for loadClips, and the two sinks the
8
+ // evaluator's outputs go to (morph weights, root motion for a character controller).
9
+ //
10
+ // `loadCreatorAnim()` must have resolved before `createAnimBridge` — the web hosts await it in their
11
+ // async setup (createViewerLite / runProject) so the SDK's first animator call finds the module ready.
12
+
13
+ import createCreatorAnim, { type CreatorAnimModule } from "../dist/creatorAnim.js"
14
+
15
+ export type Mat4Like = Float32Array | number[]
16
+
17
+ export type AnimHost = {
18
+ /** Entity lookup (undefined when unknown). `skinJoint` marks joints of a GLB skin. */
19
+ get(id: number): { id: number, name: string, m: Mat4Like, parent: number, children: number[], skinJoint?: boolean } | undefined
20
+ /** A joint's local matrix was rewritten (WebGL: worldMatrixNeedsUpdate). */
21
+ localMatrixChanged(id: number): void
22
+ /** Fetched buffer for a systemId (loadClips). */
23
+ getBuffer(systemId: number): ArrayBuffer | undefined
24
+ /** Morph-target weights sampled for an entity (WebGL: the node's blendshape weights). */
25
+ setMorphWeights?(entityId: number, weights: Float32Array): void
26
+ /** World matrix of a node — footsteps are reported in world space. Without it the animator node's
27
+ * own local matrix is used (right whenever the character hangs directly off the scene root). */
28
+ world?(entityId: number): Mat4Like | undefined
29
+ /** Root motion for a CharacterController on the node or an ancestor: the node-local delta this frame.
30
+ * Return true when a controller took it (as a velocity); false → the evaluator moves the node. */
31
+ applyRootMotion?(entityId: number, dx: number, dy: number, dz: number, dt: number, yaw: number): boolean
32
+ }
33
+
34
+ export type ClipInfo = { name: string, duration: number, trackCount: number }
35
+
36
+ let modulePromise: Promise<CreatorAnimModule> | null = null
37
+ let loaded: CreatorAnimModule | null = null
38
+ /** The wasm module, loaded once per process. */
39
+ export const loadCreatorAnim = (): Promise<CreatorAnimModule> => (modulePromise ??= createCreatorAnim().then((m) => (loaded = m)))
40
+ export const isCreatorAnimLoaded = (): boolean => loaded !== null
41
+
42
+ // ---- heap helpers --------------------------------------------------------------------------------
43
+
44
+ const withF32 = <T>(mod: CreatorAnimModule, values: ArrayLike<number>, fn: (ptr: number) => T): T => {
45
+ const ptr = mod._malloc(Math.max(4, values.length * 4))
46
+ mod.HEAPF32.set(values, ptr >> 2)
47
+ try { return fn(ptr) } finally { mod._free(ptr) }
48
+ }
49
+ const withU16 = <T>(mod: CreatorAnimModule, values: ArrayLike<number>, fn: (ptr: number) => T): T => {
50
+ const ptr = mod._malloc(Math.max(4, values.length * 2))
51
+ mod.HEAPU16.set(values, ptr >> 1)
52
+ try { return fn(ptr) } finally { mod._free(ptr) }
53
+ }
54
+ const withString = <T>(mod: CreatorAnimModule, s: string, fn: (ptr: number) => T): T => {
55
+ const n = mod.lengthBytesUTF8(s) + 1
56
+ const ptr = mod._malloc(n)
57
+ mod.stringToUTF8(s, ptr, n)
58
+ try { return fn(ptr) } finally { mod._free(ptr) }
59
+ }
60
+ const withBytes = <T>(mod: CreatorAnimModule, bytes: Uint8Array, fn: (ptr: number) => T): T => {
61
+ const ptr = mod._malloc(Math.max(1, bytes.length))
62
+ mod.HEAPU8.set(bytes, ptr)
63
+ try { return fn(ptr) } finally { mod._free(ptr) }
64
+ }
65
+
66
+ // ---- matrix helpers (column-major 4×4) -----------------------------------------------------------
67
+
68
+ /** T·R·S into a column-major 4×4 (no allocation). */
69
+ const composeInto = (m: Mat4Like, tx: number, ty: number, tz: number, x: number, y: number, z: number, w: number, sx: number, sy: number, sz: number): void => {
70
+ const x2 = x + x, y2 = y + y, z2 = z + z
71
+ const xx = x * x2, xy = x * y2, xz = x * z2, yy = y * y2, yz = y * z2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2
72
+ const a = m as number[]
73
+ a[0] = (1 - (yy + zz)) * sx; a[1] = (xy + wz) * sx; a[2] = (xz - wy) * sx; a[3] = 0
74
+ a[4] = (xy - wz) * sy; a[5] = (1 - (xx + zz)) * sy; a[6] = (yz + wx) * sy; a[7] = 0
75
+ a[8] = (xz + wy) * sz; a[9] = (yz - wx) * sz; a[10] = (1 - (xx + yy)) * sz; a[11] = 0
76
+ a[12] = tx; a[13] = ty; a[14] = tz; a[15] = 1
77
+ }
78
+
79
+ /** Column-major 4×4 → translation, rotation quat (x,y,z,w), scale. */
80
+ export const decomposeTRS = (m: Mat4Like): { t: [number, number, number], r: [number, number, number, number], s: [number, number, number] } => {
81
+ const a = m as ArrayLike<number>
82
+ let sx = Math.hypot(a[0], a[1], a[2]), sy = Math.hypot(a[4], a[5], a[6]), sz = Math.hypot(a[8], a[9], a[10])
83
+ const det = a[0] * (a[5] * a[10] - a[6] * a[9]) - a[4] * (a[1] * a[10] - a[2] * a[9]) + a[8] * (a[1] * a[6] - a[2] * a[5])
84
+ if (det < 0) sx = -sx
85
+ const ix = sx !== 0 ? 1 / sx : 0, iy = sy !== 0 ? 1 / sy : 0, iz = sz !== 0 ? 1 / sz : 0
86
+ const m00 = a[0] * ix, m10 = a[1] * ix, m20 = a[2] * ix
87
+ const m01 = a[4] * iy, m11 = a[5] * iy, m21 = a[6] * iy
88
+ const m02 = a[8] * iz, m12 = a[9] * iz, m22 = a[10] * iz
89
+ const trace = m00 + m11 + m22
90
+ let x: number, y: number, z: number, w: number
91
+ if (trace > 0) {
92
+ const s = 0.5 / Math.sqrt(trace + 1)
93
+ w = 0.25 / s; x = (m21 - m12) * s; y = (m02 - m20) * s; z = (m10 - m01) * s
94
+ } else if (m00 > m11 && m00 > m22) {
95
+ const s = 2 * Math.sqrt(1 + m00 - m11 - m22)
96
+ w = (m21 - m12) / s; x = 0.25 * s; y = (m01 + m10) / s; z = (m02 + m20) / s
97
+ } else if (m11 > m22) {
98
+ const s = 2 * Math.sqrt(1 + m11 - m00 - m22)
99
+ w = (m02 - m20) / s; x = (m01 + m10) / s; y = 0.25 * s; z = (m12 + m21) / s
100
+ } else {
101
+ const s = 2 * Math.sqrt(1 + m22 - m00 - m11)
102
+ w = (m10 - m01) / s; x = (m02 + m20) / s; y = (m12 + m21) / s; z = 0.25 * s
103
+ }
104
+ return { t: [ a[12], a[13], a[14] ], r: [ x, y, z, w ], s: [ sx, sy, sz ] }
105
+ }
106
+
107
+ /** m = m · R_y(a): a turn about the matrix's own up axis. */
108
+ const rotateLocalY = (m: Mat4Like, a: number): void => {
109
+ const c = Math.cos(a), s = Math.sin(a)
110
+ const x0 = m[0], x1 = m[1], x2 = m[2], z0 = m[8], z1 = m[9], z2 = m[10]
111
+ m[0] = x0 * c - z0 * s; m[1] = x1 * c - z1 * s; m[2] = x2 * c - z2 * s
112
+ m[8] = x0 * s + z0 * c; m[9] = x1 * s + z1 * c; m[10] = x2 * s + z2 * c
113
+ }
114
+
115
+ // ---- the bridge ----------------------------------------------------------------------------------
116
+
117
+ type Rec = {
118
+ rootId: number
119
+ entities: number[] // library joint index → entity id (the pre-order the skeleton was described in)
120
+ trs: number // heap: 10 floats per joint
121
+ write: number // heap: 1 byte per joint
122
+ n: number
123
+ feetOn: boolean // the feet stage (lock / ground IK) is on: answer its ground probes each frame
124
+ // IK chains: the library handle, the nodes read each late pass (0 = none), the parameter shadow the
125
+ // node reads are merged into before every solve (CANIM_IK_* order)
126
+ iks: { ik: number, target: number, pole: number, rotation: number, params: Float32Array }[]
127
+ // node-driven sockets: the empty read each late pass
128
+ sockets: { set: string, name: string, node: number }[]
129
+ }
130
+
131
+ // A wasm predating the chains (the vendored build not yet rebuilt): warn once, keep the chains inert.
132
+ let warnedStale = false
133
+ const hasChains = (mod: CreatorAnimModule): boolean => {
134
+ if (typeof mod._canimAnimatorLateSolve === "function") return true
135
+ if (!warnedStale) { warnedStale = true; console.warn("creator-anim: this wasm predates IK chains / sockets — rebuild it (build-wasm.sh + vendor:creator); chains are inert") }
136
+ return false
137
+ }
138
+
139
+ const IK_COUNT = 19
140
+ const IK = { TARGET_X: 0, POLE_X: 3, HAS_POLE: 6, ROT_X: 8 }
141
+
142
+ const MAX_EVENTS = 64
143
+ const IDENTITY: Mat4Like = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]
144
+
145
+ export function createAnimBridge(host: AnimHost) {
146
+ const mod = loaded
147
+ if (!mod) throw new Error("creator-anim: await loadCreatorAnim() before createAnimBridge()")
148
+ const animators = new Map<number, Rec>()
149
+ let onEvent: ((animatorId: number, slot: number, type: number) => void) | null = null
150
+ // scratch heap cells, allocated once
151
+ const out4 = mod._malloc(16)
152
+ const out3 = mod._malloc(12)
153
+ const m16 = mod._malloc(64) // the animator node's world matrix, handed over per frame
154
+ const probeFrom = mod._malloc(12), probeTo = mod._malloc(12), out8 = mod._malloc(32)
155
+ const i32a = mod._malloc(4), u32a = mod._malloc(4)
156
+ const evPairs = mod._malloc(MAX_EVENTS * 4)
157
+ const pending: { id: number, slot: number, type: number }[] = []
158
+ const steps: { id: number, side: number, x: number, y: number, z: number }[] = []
159
+ let onStep: ((animatorId: number, side: number, x: number, y: number, z: number) => void) | null = null
160
+
161
+ const loadSet = (buf: ArrayBuffer | Uint8Array): number => {
162
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
163
+ return withBytes(mod, bytes, (ptr) => mod._canimClipsLoad(ptr, bytes.length))
164
+ }
165
+
166
+ const freeRec = (rec: Rec): void => { mod._free(rec.trs); mod._free(rec.write) }
167
+
168
+ const methods = {
169
+ loadClips(systemId: number, onComplete: (clipSetId: number) => void, onReject: (err: unknown) => void): void {
170
+ const buf = host.getBuffer(systemId)
171
+ const set = buf ? loadSet(buf) : 0
172
+ if (!set) { onReject(new Error("loadClips: not a GLB with animations")); return }
173
+ onComplete(set)
174
+ },
175
+ createClipFromTracks(names: string, data: Float32Array): number {
176
+ return withString(mod, names, (np) => withF32(mod, data, (dp) => mod._canimClipsFromTracks(np, dp, data.length)))
177
+ },
178
+ getClipSetInfo(clipSetId: number): ClipInfo[] {
179
+ const n = mod._canimClipsCount(clipSetId)
180
+ const out: ClipInfo[] = []
181
+ for (let i = 0; i < n; i++) {
182
+ const np = mod._canimClipName(clipSetId, i)
183
+ out.push({ name: np ? mod.UTF8ToString(np) : "", duration: mod._canimClipDuration(clipSetId, i), trackCount: mod._canimClipTrackCount(clipSetId, i) })
184
+ }
185
+ return out
186
+ },
187
+ sliceClip(clipSetId: number, clip: number, start: number, end: number): number { return mod._canimClipsSlice(clipSetId, clip, start, end) },
188
+ /** The hosts' one derivation (`AnimationClip.from`): the mirror if `mirror`, then the [start, end] window
189
+ * (start < 0 = whole, end < 0 = the clip's end), as a NEW single-clip set. 0 = bad id / range / no rig. */
190
+ deriveClip(clipSetId: number, clip: number, mirror: boolean, start: number, end: number): number {
191
+ return mod._canimClipsDerive(clipSetId, clip, mirror ? 1 : 0, start, end)
192
+ },
193
+ destroyClipSet(clipSetId: number): void { mod._canimClipsDestroy(clipSetId) },
194
+ animatorCreate(entityId: number): number {
195
+ const root = host.get(entityId)
196
+ if (!root) return 0
197
+ const entities: number[] = [], parents: number[] = [], names: string[] = [], rest: number[] = [], skin: number[] = []
198
+ const collect = (id: number, parent: number): void => {
199
+ const e = host.get(id)
200
+ if (!e) return
201
+ const index = entities.length
202
+ entities.push(id); parents.push(parent); names.push(e.name ?? ""); skin.push(e.skinJoint ? 1 : 0)
203
+ const { t, r, s } = decomposeTRS(e.m)
204
+ rest.push(t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2])
205
+ for (const c of e.children) collect(c, index)
206
+ }
207
+ collect(entityId, -1)
208
+ const n = entities.length
209
+ const parentsPtr = mod._malloc(Math.max(4, n * 4))
210
+ mod.HEAP32.set(parents, parentsPtr >> 2)
211
+ const skinPtr = mod._malloc(Math.max(1, n))
212
+ mod.HEAPU8.set(skin, skinPtr)
213
+ let id = 0
214
+ try {
215
+ id = withString(mod, names.join("\n"), (np) => withF32(mod, rest, (rp) => mod._canimAnimatorCreate(n, parentsPtr, np, rp, skinPtr, entityId)))
216
+ } finally { mod._free(parentsPtr); mod._free(skinPtr) }
217
+ if (!id) return 0
218
+ const shadowed = mod._canimAnimatorShadowedNames(id)
219
+ if (shadowed > 0) console.warn(`[Animator] ${shadowed} bone name(s) exist twice under this model (a leftover skeleton copy?) — bound to the skinned ones; run 'lecodes assets doctor' on the GLB`)
220
+ animators.set(id, { rootId: entityId, entities, trs: mod._malloc(Math.max(4, n * 40)), write: mod._malloc(Math.max(1, n)), n, feetOn: false, iks: [], sockets: [] })
221
+ return id
222
+ },
223
+ animatorDestroy(animatorId: number): void {
224
+ const rec = animators.get(animatorId)
225
+ if (!rec) { mod._canimAnimatorDestroy(animatorId); return }
226
+ // Leave the skeleton in rest pose (the next animator snapshots it as its rest pose).
227
+ mod._canimAnimatorRest(animatorId, rec.trs, rec.write)
228
+ const f = mod.HEAPF32, u = mod.HEAPU8, base = rec.trs >> 2
229
+ for (let j = 0; j < rec.n; j++) {
230
+ if (!u[rec.write + j]) continue
231
+ const e = host.get(rec.entities[j])
232
+ if (!e) continue
233
+ const o = base + j * 10
234
+ composeInto(e.m, f[o], f[o + 1], f[o + 2], f[o + 3], f[o + 4], f[o + 5], f[o + 6], f[o + 7], f[o + 8], f[o + 9])
235
+ host.localMatrixChanged(rec.entities[j])
236
+ }
237
+ mod._canimAnimatorDestroy(animatorId)
238
+ freeRec(rec)
239
+ animators.delete(animatorId)
240
+ },
241
+ animatorBind(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number { return mod._canimAnimatorBind(animatorId, clipSetId, clipIndex, layer) },
242
+ animatorBindMirror(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number { return mod._canimAnimatorBindMirror ? mod._canimAnimatorBindMirror(animatorId, clipSetId, clipIndex, layer) : -1 },
243
+ animatorBoundTracks(animatorId: number, slot: number): number { return mod._canimAnimatorBoundTracks(animatorId, slot) },
244
+ setClipEvents(clipSetId: number, clip: number, times: Float32Array): void {
245
+ withF32(mod, times, (tp) => mod._canimClipsSetEvents(clipSetId, clip, tp, times.length))
246
+ },
247
+ animatorSetBlend(animatorId: number, layer: number, dims: number, slots: Uint16Array, positions: Float32Array, fade: number, speed: number, phases?: Float32Array): void {
248
+ const d = dims > 1 ? 2 : 1
249
+ const n = Math.min(slots.length, Math.floor(positions.length / d))
250
+ const ph = phases && phases.length >= n * 2 ? phases : null
251
+ withU16(mod, slots, (sp) => withF32(mod, positions, (pp) => {
252
+ if (ph) withF32(mod, ph, (hp) => mod._canimAnimatorSetBlend(animatorId, layer, d, sp, pp, hp, n, fade, speed))
253
+ else mod._canimAnimatorSetBlend(animatorId, layer, d, sp, pp, 0, n, fade, speed)
254
+ }))
255
+ },
256
+ animatorSetBlendValue(animatorId: number, layer: number, x: number, y: number): void { mod._canimAnimatorSetBlendValue(animatorId, layer, x, y) },
257
+ animatorSlotSetTurn(animatorId: number, slot: number, radians: number, enabled: boolean): void { mod._canimAnimatorSlotSetTurn(animatorId, slot, radians, enabled ? 1 : 0) },
258
+ animatorSlotSetWindow(animatorId: number, slot: number, start: number, end: number): void { mod._canimAnimatorSlotSetWindow(animatorId, slot, start, end) },
259
+ animatorPlay(animatorId: number, slot: number, loop: boolean, speed: number, fadeIn: number, fadeOut: number, restart: boolean): void {
260
+ mod._canimAnimatorPlay(animatorId, slot, loop ? 1 : 0, speed, fadeIn, fadeOut, restart ? 1 : 0)
261
+ },
262
+ animatorStop(animatorId: number, layer: number, slot: number, fade: number): void { mod._canimAnimatorStop(animatorId, layer, slot, fade) },
263
+ animatorSeek(animatorId: number, slot: number, time: number): void { mod._canimAnimatorSeek(animatorId, slot, time) },
264
+ animatorGetSlotTime(animatorId: number, slot: number): number { return mod._canimAnimatorSlotTime(animatorId, slot) },
265
+ animatorGetSlotWeight(animatorId: number, slot: number): number { return mod._canimAnimatorSlotWeight(animatorId, slot) },
266
+ animatorSetLayer(animatorId: number, layer: number, weight: number, additive: boolean, maskRoot: string): void {
267
+ withString(mod, maskRoot ?? "", (mp) => mod._canimAnimatorSetLayer(animatorId, layer, weight, additive ? 1 : 0, mp))
268
+ },
269
+ animatorSetLayerAnticipation(animatorId: number, layer: number, amount: number): void { mod._canimAnimatorSetLayerAnticipation(animatorId, layer, amount) },
270
+ animatorSetGlobal(animatorId: number, speed: number, paused: boolean): void { mod._canimAnimatorSetGlobal(animatorId, speed, paused ? 1 : 0) },
271
+ /** How far joints rotate over every clip bound to the animator that drives `jointEntityId`, per
272
+ * (child entity, parent entity) pair against `frames` (8 per pair: the reference relative rotation
273
+ * xyzw, the measuring frame xyzw in the parent's space) — creator-anim's canimAnimatorJointRanges.
274
+ * Returns 6 radians per pair (twist / plane / normal min-max), or null when no animator carries
275
+ * the joint or nothing is bound yet. */
276
+ animatorJointRanges(jointEntityId: number, pairs: ArrayLike<number>, frames: Float32Array): Float32Array | null {
277
+ for (const [ id, rec ] of animators) {
278
+ if (!rec.entities.includes(jointEntityId)) continue
279
+ const count = pairs.length >> 1
280
+ const idx = new Int32Array(count * 2)
281
+ for (let i = 0; i < count * 2; i++) idx[i] = rec.entities.indexOf(pairs[i])
282
+ const out = new Float32Array(count * 6)
283
+ const ptrI = mod._malloc(Math.max(4, count * 8))
284
+ mod.HEAP32.set(idx, ptrI >> 2)
285
+ const ptrO = mod._malloc(Math.max(4, count * 24))
286
+ let sampled = 0
287
+ try {
288
+ sampled = withF32(mod, frames, (fp) => mod._canimAnimatorJointRanges(id, ptrI, count, fp, ptrO))
289
+ if (sampled > 0) out.set(mod.HEAPF32.subarray(ptrO >> 2, (ptrO >> 2) + count * 6))
290
+ } finally { mod._free(ptrI); mod._free(ptrO) }
291
+ return sampled > 0 ? out : null
292
+ }
293
+ return null
294
+ },
295
+ /** The joints' CURRENT locals become what the animator driving `jointEntityId` showed: the next
296
+ * evaluation transitions from them over `blend` seconds (a ragdoll let go — creator-gl's
297
+ * seedAnimatorFromEntities). No animator carries the joint = nothing happens. */
298
+ animatorSeedPose(jointEntityId: number, blend: number): void {
299
+ for (const [ id, rec ] of animators) {
300
+ if (!rec.entities.includes(jointEntityId)) continue
301
+ const trs = new Float32Array(rec.n * 10)
302
+ for (let j = 0; j < rec.n; j++) {
303
+ const e = host.get(rec.entities[j])
304
+ const { t, r, s } = decomposeTRS(e ? e.m : IDENTITY)
305
+ trs.set([ t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2] ], j * 10)
306
+ }
307
+ withF32(mod, trs, (tp) => mod._canimAnimatorSeedPose(id, tp, blend))
308
+ return
309
+ }
310
+ },
311
+ animatorSetRootMotion(animatorId: number, bone: string, apply: number): void {
312
+ withString(mod, bone ?? "", (bp) => mod._canimAnimatorSetRootMotion(animatorId, bp, (apply | 0) & 255))
313
+ },
314
+ animatorGetRootMotion(animatorId: number, out: Float32Array): void {
315
+ mod._canimAnimatorRootMotion(animatorId, out3)
316
+ const f = mod.HEAPF32, o = out3 >> 2
317
+ out[0] = f[o]; out[1] = f[o + 1]; out[2] = f[o + 2]
318
+ },
319
+ setOnAnimatorEvent(callback: (animatorId: number, slot: number, type: number) => void): void { onEvent = callback },
320
+
321
+ // ---- contacts, phase, root curves (docs/animation-v2-plan.md §2.5-2.6) ----
322
+ animatorSetFeet(animatorId: number, left: string, right: string): void {
323
+ withString(mod, left, (l) => withString(mod, right, (r) => mod._canimAnimatorSetFeet(animatorId, l, r)))
324
+ },
325
+ animatorSlotCurveAt(animatorId: number, slot: number, which: number, time: number): number {
326
+ return mod._canimAnimatorSlotCurveAt(animatorId, slot, which, time)
327
+ },
328
+ animatorSlotCurveInfo(animatorId: number, slot: number, out: Float32Array): boolean {
329
+ const ok = mod._canimAnimatorSlotCurveInfo(animatorId, slot, out4)
330
+ if (ok) out.set(mod.HEAPF32.subarray(out4 >> 2, (out4 >> 2) + Math.min(4, out.length)))
331
+ return ok !== 0
332
+ },
333
+ animatorSlotTurn(animatorId: number, slot: number): number { return mod._canimAnimatorSlotTurn(animatorId, slot) },
334
+ animatorSlotTimeAtTurn(animatorId: number, slot: number, yaw: number): number { return mod._canimAnimatorSlotTimeAtTurn(animatorId, slot, yaw) },
335
+ animatorLayerPhase(animatorId: number, layer: number): number { return mod._canimAnimatorLayerPhase(animatorId, layer) },
336
+ animatorSeekPhase(animatorId: number, slot: number, phase: number): void { mod._canimAnimatorSeekPhase(animatorId, slot, phase) },
337
+ animatorSlotContacts(animatorId: number, slot: number, out: Float32Array): number {
338
+ const maxSpans = Math.floor(out.length / 8) // stride 8 since 2026-09-01 (+ stillFrom/stillTo)
339
+ if (maxSpans === 0) return mod._canimAnimatorSlotContacts(animatorId, slot, 0, 0)
340
+ const ptr = mod._malloc(maxSpans * 8 * 4)
341
+ const n = mod._canimAnimatorSlotContacts(animatorId, slot, ptr, maxSpans)
342
+ out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(n, maxSpans) * 8))
343
+ mod._free(ptr)
344
+ return n
345
+ },
346
+ animatorSlotTreads(animatorId: number, slot: number, out: Float32Array): number {
347
+ if (out.length === 0) return mod._canimAnimatorSlotTreads(animatorId, slot, 0, 0)
348
+ const ptr = mod._malloc(out.length * 4)
349
+ const n = mod._canimAnimatorSlotTreads(animatorId, slot, ptr, out.length)
350
+ out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(n + 1, out.length)))
351
+ mod._free(ptr)
352
+ return n
353
+ },
354
+ animatorSlotPhysics(animatorId: number, slot: number, time: number, out: Float32Array): number {
355
+ const ptr = mod._malloc(11 * 4)
356
+ const n = mod._canimAnimatorSlotPhysics(animatorId, slot, time, ptr)
357
+ if (n) out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(11, out.length)))
358
+ mod._free(ptr)
359
+ return n
360
+ },
361
+ animatorSlotFeatures(animatorId: number, slot: number, time: number, out: Float32Array): number {
362
+ const ptr = mod._malloc(37 * 4)
363
+ const n = mod._canimAnimatorSlotFeatures(animatorId, slot, time, ptr)
364
+ if (n) out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(37, out.length)))
365
+ mod._free(ptr)
366
+ return n
367
+ },
368
+ animatorKneeAxis(animatorId: number, side: number, out: Float32Array): number {
369
+ const n = mod._canimAnimatorKneeAxis(animatorId, side, out8)
370
+ if (n) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(8, out.length)))
371
+ return n
372
+ },
373
+ animatorSlotKneePole(animatorId: number, slot: number, side: number, time: number, out: Float32Array): number {
374
+ const n = mod._canimAnimatorSlotKneePole(animatorId, slot, side, time, out8)
375
+ if (n) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(6, out.length)))
376
+ return n
377
+ },
378
+ animatorSetStepWarp(animatorId: number, on: boolean, stride: number, lift: number, pitchDeg: number, slopeDeg: number): void {
379
+ mod._canimAnimatorSetStepWarp(animatorId, on ? 1 : 0, stride, lift, pitchDeg, slopeDeg)
380
+ },
381
+ animatorSlotFit(animatorId: number, slot: number, target: number, align: number, out: Float32Array): number {
382
+ if (out.length === 0) return mod._canimAnimatorSlotFit(animatorId, slot, target, align, 0, 0)
383
+ const ptr = mod._malloc(out.length * 4)
384
+ const n = mod._canimAnimatorSlotFit(animatorId, slot, target, align, ptr, out.length)
385
+ out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(n, out.length)))
386
+ mod._free(ptr)
387
+ return n
388
+ },
389
+ animatorSlotExit(animatorId: number, slot: number, target: number, tolerance: number, atContact: boolean, align: number): number {
390
+ return mod._canimAnimatorSlotExit(animatorId, slot, target, tolerance, atContact ? 1 : 0, align)
391
+ },
392
+ animatorSlotAlign(animatorId: number, a: number, b: number, offA: number, cyclesA: number, out: Float32Array): number {
393
+ if (out.length < 4) return 0
394
+ const ptr = mod._malloc(16)
395
+ const ok = mod._canimAnimatorSlotAlign(animatorId, a, b, offA, cyclesA, ptr)
396
+ if (ok) out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + 4))
397
+ mod._free(ptr)
398
+ return ok
399
+ },
400
+ setOnAnimatorStep(callback: (animatorId: number, side: number, x: number, y: number, z: number) => void): void { onStep = callback },
401
+
402
+ // ---- feet: foot lock + ground IK (docs/animation-v2-plan.md §2.9) ----
403
+ animatorSetFeetParams(animatorId: number, params: Float32Array): void {
404
+ withF32(mod, params, (p) => mod._canimAnimatorSetFeetParams(animatorId, p, params.length))
405
+ const rec = animators.get(animatorId)
406
+ if (rec) rec.feetOn = (params.length > 0 && params[0] > 0.5) || (params.length > 1 && params[1] > 0.5)
407
+ },
408
+ animatorFootState(animatorId: number, side: number, out: Float32Array): boolean {
409
+ const ok = mod._canimAnimatorFootState(animatorId, side, out8)
410
+ if (ok) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(8, out.length)))
411
+ return ok !== 0
412
+ },
413
+
414
+ // ---- IK chains + sockets (creator-anim anchors.cpp): solved in the LATE pass (lateSolve below) ----
415
+ /** kind 0 two-bone (the end entity, its parent, its grandparent) / 1 look-at. 0 = no such joint. */
416
+ animatorIkCreate(animatorId: number, kind: number, endEntityId: number): number {
417
+ const rec = animators.get(animatorId)
418
+ if (!rec || !hasChains(mod)) return 0
419
+ const end = rec.entities.indexOf(endEntityId)
420
+ if (end < 0) return 0
421
+ let mid = -1, root = -1
422
+ if (kind === 0) {
423
+ const midE = host.get(endEntityId)?.parent ?? 0
424
+ mid = rec.entities.indexOf(midE)
425
+ if (mid < 0) return 0
426
+ root = rec.entities.indexOf(host.get(midE)?.parent ?? 0)
427
+ if (root < 0) return 0
428
+ }
429
+ const ik = mod._canimAnimatorIkCreateJoints(animatorId, kind, root, mid, end)
430
+ if (!ik) return 0
431
+ const params = new Float32Array(IK_COUNT)
432
+ const ptr = mod._malloc(IK_COUNT * 4)
433
+ mod._canimAnimatorIkGet(animatorId, ik, ptr, IK_COUNT)
434
+ params.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + IK_COUNT))
435
+ mod._free(ptr)
436
+ rec.iks.push({ ik, target: 0, pole: 0, rotation: 0, params })
437
+ return ik
438
+ },
439
+ animatorIkDestroy(animatorId: number, ik: number): void {
440
+ mod._canimAnimatorIkDestroy(animatorId, ik)
441
+ const rec = animators.get(animatorId)
442
+ if (rec) rec.iks = rec.iks.filter((r) => r.ik !== ik)
443
+ },
444
+ animatorIkSet(animatorId: number, ik: number, params: Float32Array): void {
445
+ withF32(mod, params, (p) => mod._canimAnimatorIkSet(animatorId, ik, p, params.length))
446
+ const r = animators.get(animatorId)?.iks.find((x) => x.ik === ik)
447
+ if (r) r.params.set(params.subarray(0, Math.min(params.length, IK_COUNT)))
448
+ },
449
+ animatorIkNodes(animatorId: number, ik: number, targetEntity: number, poleEntity: number, rotationEntity: number): void {
450
+ const r = animators.get(animatorId)?.iks.find((x) => x.ik === ik)
451
+ if (r) { r.target = targetEntity; r.pole = poleEntity; r.rotation = rotationEntity }
452
+ },
453
+ animatorIkAnchor(animatorId: number, ik: number, socket: string): void {
454
+ withString(mod, socket ?? "", (sp) => mod._canimAnimatorIkAnchor(animatorId, ik, sp))
455
+ },
456
+ animatorIkState(animatorId: number, ik: number, out: Float32Array): boolean {
457
+ const ok = mod._canimAnimatorIkState(animatorId, ik, out4)
458
+ if (ok) out.set(mod.HEAPF32.subarray(out4 >> 2, (out4 >> 2) + Math.min(4, out.length)))
459
+ return ok !== 0
460
+ },
461
+ /** A socket on joint `jointEntityId`: a NODE socket (nodeEntityId != 0) is read every late pass, else
462
+ * `trs` (7 floats) fixed in the joint's space; neither = remove. */
463
+ animatorSetSocket(animatorId: number, set: string, name: string, jointEntityId: number, nodeEntityId: number, trs: Float32Array | null): void {
464
+ const rec = animators.get(animatorId)
465
+ if (!rec || !hasChains(mod)) return
466
+ rec.sockets = rec.sockets.filter((s) => !(s.set === set && s.name === name))
467
+ const call = (joint: number, values: ArrayLike<number> | null): void => {
468
+ withString(mod, set, (sp) => withString(mod, name, (np) => {
469
+ if (values) withF32(mod, values, (vp) => mod._canimAnimatorSetSocket(animatorId, sp, np, joint, vp))
470
+ else mod._canimAnimatorSetSocket(animatorId, sp, np, joint, 0)
471
+ }))
472
+ }
473
+ if (nodeEntityId === 0 && !trs) { call(-1, null); return }
474
+ const joint = rec.entities.indexOf(jointEntityId)
475
+ if (joint < 0) { console.warn(`[Animator] socket '${name}': the joint is no bone of this animator`); return }
476
+ call(joint, trs ?? [ 0, 0, 0, 0, 0, 0, 1 ])
477
+ if (nodeEntityId !== 0) rec.sockets.push({ set, name, node: nodeEntityId })
478
+ },
479
+ setClipAnchors(clipSetId: number, clip: number, bone: string, donor: string, sockets: string, spans: Float32Array): void {
480
+ if (!hasChains(mod)) return
481
+ withString(mod, bone, (bp) => withString(mod, donor ?? "", (dp) => withString(mod, sockets ?? "", (sp) =>
482
+ withF32(mod, spans, (vp) => mod._canimClipsSetAnchors(clipSetId, clip, bp, dp, sp, vp, Math.floor(spans.length / 4))))))
483
+ },
484
+
485
+ // ---- drives + warping (docs/animation-v2-plan.md §2.6 / §2.7) — the same `_creator` shapes creator-pkg exposes ----
486
+ /** The body's world velocity this frame: the game side of the stride / orientation warps' fit. */
487
+ animatorSetMotion(animatorId: number, vx: number, vy: number, vz: number): void {
488
+ mod._canimAnimatorSetMotion(animatorId, vx, vy, vz)
489
+ },
490
+ /** Cumulative capsule travel (m) and yaw (rad) — what distance / angle drives read. */
491
+ animatorSetDriveInput(animatorId: number, distance: number, angle: number): void {
492
+ mod._canimAnimatorSetDriveInput(animatorId, distance, angle)
493
+ },
494
+ /** A slot's clock driven by a quantity: mode 0 time / 1 distance / 2 angle, `entry` = the curve value at NOW. */
495
+ animatorSetSlotDrive(animatorId: number, slot: number, mode: number, entry: number): void {
496
+ mod._canimAnimatorSetSlotDrive(animatorId, slot, mode, entry)
497
+ },
498
+ /** Stride / orientation warping, as the engine's fixed-order CANIM_WARP_* parameter array. */
499
+ animatorSetWarpParams(animatorId: number, params: Float32Array): void {
500
+ withF32(mod, params, (p) => mod._canimAnimatorSetWarpParams(animatorId, p, params.length))
501
+ },
502
+ }
503
+
504
+ const evaluateOne = (id: number, rec: Rec, dt: number): void => {
505
+ // The frame the library's pose is in, before the evaluation that uses it: the post-process stage
506
+ // (warps, the feet) works in world space, and the pose includes joint 0 — the animator node's own
507
+ // local transform — so the frame is the node's PARENT world (identity for a node at the root). The
508
+ // host's `world` when it has one, else the parent's local matrix (exact whenever that parent hangs
509
+ // directly off the scene root).
510
+ const node = host.get(rec.rootId)
511
+ const parent = node && node.parent ? host.get(node.parent) : undefined
512
+ const W: Mat4Like = parent ? (host.world?.(parent.id) ?? parent.m) : IDENTITY
513
+ mod.HEAPF32.set(W as ArrayLike<number>, m16 >> 2); mod._canimAnimatorSetWorld(id, m16)
514
+ const evCount = mod._canimAnimatorEvaluate(id, dt, 0, rec.trs, rec.write)
515
+ // The feet stage's ground for the NEXT evaluation. The lite core has no raycast, so the ground is
516
+ // the plane at the node's height, normal up — enough for the lock and to keep the pelvis where it
517
+ // is; stairs and slopes are a later lite-core addition.
518
+ if (rec.feetOn && node) {
519
+ const groundY = (host.world?.(rec.rootId) ?? node.m)[13]
520
+ for (let side = 0; side < 2; side++) {
521
+ if (!mod._canimAnimatorFootProbe(id, side, probeFrom, probeTo)) continue
522
+ const f = mod.HEAPF32, o = probeFrom >> 2
523
+ mod._canimAnimatorSetGround(id, side, 1, f[o], groundY, f[o + 2], 0, 1, 0)
524
+ }
525
+ }
526
+ const f = mod.HEAPF32, u = mod.HEAPU8, base = rec.trs >> 2
527
+ for (let j = 0; j < rec.n; j++) {
528
+ if (!u[rec.write + j]) continue
529
+ const e = host.get(rec.entities[j])
530
+ if (!e) continue
531
+ const o = base + j * 10
532
+ composeInto(e.m, f[o], f[o + 1], f[o + 2], f[o + 3], f[o + 4], f[o + 5], f[o + 6], f[o + 7], f[o + 8], f[o + 9])
533
+ host.localMatrixChanged(rec.entities[j])
534
+ }
535
+ if (host.setMorphWeights) {
536
+ const morphs = mod._canimAnimatorMorphCount(id)
537
+ for (let i = 0; i < morphs; i++) {
538
+ const wp = mod._canimAnimatorMorph(id, i, i32a, u32a)
539
+ const joint = mod.HEAP32[i32a >> 2], count = mod.HEAPU32[u32a >> 2]
540
+ if (!wp || joint < 0 || count === 0) continue
541
+ host.setMorphWeights(rec.entities[joint], Float32Array.from(mod.HEAPF32.subarray(wp >> 2, (wp >> 2) + count)))
542
+ }
543
+ }
544
+ // root motion: apply this frame's horizontal travel (animator-node frame) + yaw per the apply mode
545
+ const mode = mod._canimAnimatorRootMotionFrame(id, out4)
546
+ if (mode !== 0) {
547
+ const g = mod.HEAPF32, o = out4 >> 2
548
+ const dx = g[o], dy = g[o + 1], dz = g[o + 2], yaw = g[o + 3]
549
+ let applied = false
550
+ if (mode === 2 && host.applyRootMotion) applied = host.applyRootMotion(rec.rootId, dx, dy, dz, dt, yaw)
551
+ if (!applied && (dx !== 0 || dy !== 0 || dz !== 0 || yaw !== 0)) {
552
+ const e = host.get(rec.rootId)
553
+ if (e) {
554
+ const m = e.m // → the parent's space (rotation + scale of the local matrix)
555
+ if (yaw !== 0) rotateLocalY(m, yaw) // the turn first: the travel follows the new heading
556
+ const px = m[0] * dx + m[4] * dy + m[8] * dz, py = m[1] * dx + m[5] * dy + m[9] * dz, pz = m[2] * dx + m[6] * dy + m[10] * dz
557
+ m[12] += px; m[13] += py; m[14] += pz
558
+ host.localMatrixChanged(rec.rootId)
559
+ }
560
+ }
561
+ }
562
+ // footsteps: contacts that began this evaluation, model space → world
563
+ if (onStep) {
564
+ const n = mod._canimAnimatorStepCount(id)
565
+ for (let i = 0; i < n; i++) {
566
+ const side = mod._canimAnimatorStep(id, i, out3)
567
+ if (side < 0) continue
568
+ const f = mod.HEAPF32, o = out3 >> 2
569
+ const x = f[o], y = f[o + 1], z = f[o + 2]
570
+ const m = W // the pose's frame (the node's parent), the same one the evaluation was handed
571
+ steps.push({ id, side,
572
+ x: m[0] * x + m[4] * y + m[8] * z + m[12],
573
+ y: m[1] * x + m[5] * y + m[9] * z + m[13],
574
+ z: m[2] * x + m[6] * y + m[10] * z + m[14] })
575
+ }
576
+ }
577
+ if (evCount > 0) {
578
+ const n = mod._canimAnimatorEvents(id, evPairs, MAX_EVENTS)
579
+ const p = mod.HEAPU16, o = evPairs >> 1
580
+ for (let i = 0; i < n; i++) pending.push({ id, slot: p[o + i * 2], type: p[o + i * 2 + 1] })
581
+ }
582
+ }
583
+
584
+ // The LATE pass (creator-gl's lateSolveAnimators): after the app's late phase, every animator with IK
585
+ // chains hands the library its joints AS THEY ARE NOW plus this frame's node targets and node sockets;
586
+ // the chains solve on that pose and only the joints they rotated are written back.
587
+ const worldOf = (id: number): Mat4Like | undefined => host.world?.(id) ?? host.get(id)?.m
588
+ const lateSolveOne = (id: number, rec: Rec): void => {
589
+ if (rec.iks.length === 0) return
590
+ const node = host.get(rec.rootId)
591
+ if (!node) return
592
+ const W: Mat4Like = node.parent ? (worldOf(node.parent) ?? IDENTITY) : IDENTITY
593
+ for (const r of rec.iks) {
594
+ if (!r.target && !r.pole && !r.rotation) continue
595
+ const tw = r.target ? worldOf(r.target) : undefined
596
+ if (tw) { r.params[IK.TARGET_X] = tw[12]; r.params[IK.TARGET_X + 1] = tw[13]; r.params[IK.TARGET_X + 2] = tw[14] }
597
+ const pw = r.pole ? worldOf(r.pole) : undefined
598
+ if (pw) { r.params[IK.POLE_X] = pw[12]; r.params[IK.POLE_X + 1] = pw[13]; r.params[IK.POLE_X + 2] = pw[14]; r.params[IK.HAS_POLE] = 1 }
599
+ const rw = r.rotation ? worldOf(r.rotation) : undefined
600
+ if (rw) { const q = decomposeTRS(rw).r; r.params[IK.ROT_X] = q[0]; r.params[IK.ROT_X + 1] = q[1]; r.params[IK.ROT_X + 2] = q[2]; r.params[IK.ROT_X + 3] = q[3] }
601
+ withF32(mod, r.params, (p) => mod._canimAnimatorIkSet(id, r.ik, p, IK_COUNT))
602
+ }
603
+ for (const s of rec.sockets) {
604
+ const m = worldOf(s.node)
605
+ if (!m) continue
606
+ mod.HEAPF32.set(m as ArrayLike<number>, m16 >> 2)
607
+ withString(mod, s.set, (sp) => withString(mod, s.name, (np) => mod._canimAnimatorSocketSetWorld(id, sp, np, m16)))
608
+ }
609
+ const f = mod.HEAPF32, base = rec.trs >> 2
610
+ for (let j = 0; j < rec.n; j++) {
611
+ const e = host.get(rec.entities[j])
612
+ const { t, r, s } = decomposeTRS(e ? e.m : IDENTITY)
613
+ const o = base + j * 10
614
+ f[o] = t[0]; f[o + 1] = t[1]; f[o + 2] = t[2]; f[o + 3] = r[0]; f[o + 4] = r[1]; f[o + 5] = r[2]; f[o + 6] = r[3]; f[o + 7] = s[0]; f[o + 8] = s[1]; f[o + 9] = s[2]
615
+ }
616
+ mod.HEAPF32.set(W as ArrayLike<number>, m16 >> 2)
617
+ if (mod._canimAnimatorLateSolve(id, m16, rec.trs, rec.write) === 0) return
618
+ const u = mod.HEAPU8
619
+ for (let j = 0; j < rec.n; j++) {
620
+ if (!u[rec.write + j]) continue
621
+ const e = host.get(rec.entities[j])
622
+ if (!e) continue
623
+ const o = base + j * 10
624
+ composeInto(e.m, f[o], f[o + 1], f[o + 2], f[o + 3], f[o + 4], f[o + 5], f[o + 6], f[o + 7], f[o + 8], f[o + 9])
625
+ host.localMatrixChanged(rec.entities[j])
626
+ }
627
+ }
628
+
629
+ return {
630
+ methods,
631
+ /** Register a GLB's embedded clips (parsed from its bytes) — returns the set id, 0 if none. */
632
+ registerGlbClips(buf: ArrayBuffer): number { return loadSet(buf) },
633
+ /** The late pass: solve every animator's IK chains on the joints as the app's late phase left them. */
634
+ lateSolve(): void { for (const [ id, rec ] of animators) lateSolveOne(id, rec) },
635
+ /** Advance every animator by dt (after physics, before the late phase). Events fire after every
636
+ * animator has been evaluated (a handler may play / stop / destroy animators). */
637
+ evaluate(dt: number): void {
638
+ for (const [ id, rec ] of animators) evaluateOne(id, rec, dt)
639
+ if (steps.length > 0) {
640
+ const batch = steps.splice(0, steps.length)
641
+ if (onStep) for (const s of batch) onStep(s.id, s.side, s.x, s.y, s.z)
642
+ }
643
+ if (pending.length > 0 && onEvent) {
644
+ const batch = pending.splice(0, pending.length)
645
+ for (const e of batch) onEvent(e.id, e.slot, e.type)
646
+ } else pending.length = 0
647
+ },
648
+ /** destroyEntity hook. */
649
+ onEntityDestroyed(entityId: number): void {
650
+ mod._canimAnimatorsDestroyTagged(entityId)
651
+ for (const [ id, rec ] of animators) if (rec.rootId === entityId) { freeRec(rec); animators.delete(id) }
652
+ },
653
+ }
654
+ }
655
+
656
+ export type AnimBridge = ReturnType<typeof createAnimBridge>