@lovelace_lol/embody 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ Embody provides mappings that connect [Facial Action Coding System (FACS)](https
7
7
 
8
8
  > **Note:** If you previously used the `loomlarge` npm package, it has been renamed to `@lovelace_lol/embody`.
9
9
  >
10
- > Continuous PR releases are published via [pkg.pr.new](https://pkg.pr.new/). Stable npm publishing is deferred until we have a release candidate; until then consumers should use git SHA pins or pkg.pr.new preview URLs. Install the [pkg-pr-new GitHub App](https://github.com/apps/pkg-pr-new) on this repository so PR workflows can publish previews.
10
+ > Stable releases publish to npm as `@lovelace_lol/embody`. Consumers should depend on the published npm package (or an exact git SHA only when the package is installable from source).
11
11
 
12
12
  ![Hero image showing Embody controlling a facial expression](./assets/readme/hero-expressions.webp)
13
13
 
@@ -5,6 +5,7 @@ export interface WasmRuntimeCoreOptions {
5
5
  readonly profile: Profile;
6
6
  readonly model: ModelDescriptor;
7
7
  readonly wasm: EmbodyCoreWasmModule;
8
+ readonly allowEmptyBindings?: boolean;
8
9
  }
9
10
  /**
10
11
  * TypeScript facade over the host-neutral Rust Wasm RuntimeCore.
@@ -13,12 +14,19 @@ export interface WasmRuntimeCoreOptions {
13
14
  export declare class WasmRuntimeCore {
14
15
  private readonly wasm;
15
16
  private readonly core;
17
+ private readonly allowEmptyBindings;
16
18
  private profile;
17
19
  private model;
18
20
  constructor(options: WasmRuntimeCoreOptions);
19
21
  setProfile(profile: Profile): void;
20
22
  setModelDescriptor(model: ModelDescriptor): void;
21
23
  setAU(id: number, value: number, balance?: number): void;
24
+ setContinuum(negativeAU: number, positiveAU: number, value: number, balance?: number): void;
25
+ transitionAU(id: number, to: number, durationMs: number, balance?: number): void;
26
+ transitionViseme(visemeIndex: number, to: number, durationMs: number, jawScale?: number): void;
27
+ update(deltaSeconds: number): number;
28
+ activeTransitionCount(): number;
29
+ clearTransitions(): void;
22
30
  getAU(id: number): number;
23
31
  setAUMixWeight(id: number, weight: number): void;
24
32
  setViseme(visemeIndex: number, value: number, jawScale?: number): void;
@@ -1,8 +1,6 @@
1
1
  export type { BoneDescriptor, BoneFrameDelta, BoneId, BoneTransformTrackTarget, ChannelId, ClipChannelIR, ClipChannelKind, ClipIR, ClipInterpolation, ClipTrackIR, ClipTrackTarget, ClipTrackTargetKind, ClipTrackTransformProperty, ClipTrackValueType, FrameDelta, FrameValueMode, HostAnimationHandle, HostAnimationPlayOptions, HostAnimationSystem, HostFrameApplier, HostModelInspector, HostTargetId, MeshDescriptor, MeshFrameDelta, MeshId, MeshVisibilityTrackTarget, ModelDescriptor, MorphTargetDescriptor, MorphTargetFrameDelta, MorphTargetId, MorphTargetTrackTarget, NumericArray, PackedMeshProportions, PackedTemplateSkeletonFitSolution, Quat, MeshProportionsField, TemplateSkeletonFitSolutionField, TemplateSkeletonFitManualAdjustment, TemplateSkeletonFitMetadata, TemplateSkeletonFitMetrics, TemplateSkeletonFitStatus, TemplateSkeletonFitTransform, TemplateSkeletonFitTransformField, TemplateSkeletonFitValidationResult, TemplateSkeletonFitVerticalAnchor, TemplateSkeletonFitVerticalAxis, TrackId, Transform, TransformSpace, Vec3, } from './contracts';
2
2
  export { TEMPLATE_SKELETON_FIT_METADATA_KIND, TEMPLATE_SKELETON_FIT_METADATA_VERSION, MESH_PROPORTIONS_FIELDS, MESH_PROPORTIONS_STRIDE, TEMPLATE_SKELETON_FIT_STATUSES, TEMPLATE_SKELETON_FIT_SOLUTION_FIELDS, TEMPLATE_SKELETON_FIT_SOLUTION_STRIDE, TEMPLATE_SKELETON_FIT_TRANSFORM_FIELDS, TEMPLATE_SKELETON_FIT_TRANSFORM_STRIDE, TEMPLATE_SKELETON_FIT_VERTICAL_ANCHORS, TEMPLATE_SKELETON_FIT_VERTICAL_AXES, composeTemplateSkeletonFitTransform, isTemplateSkeletonFitStatus, validateTemplateSkeletonFitMetadata, } from './contracts';
3
3
  export { TsClipCompiler, numericArrayToNumbers, } from './TsClipCompiler';
4
- export { TsRuntimeCore } from './TsRuntimeCore';
5
4
  export { WasmRuntimeCore, unpackMorphFrameDelta, unpackBoneFrameDelta, } from './WasmRuntimeCore';
6
5
  export type { WasmRuntimeCoreOptions, } from './WasmRuntimeCore';
7
6
  export type { TsClipCompilerInput, TsClipCompilerKeyframe, TsClipCompilerTrackInput, TsClipCurveTarget, TsClipCurvesInput, } from './TsClipCompiler';
8
- export type { TsRuntimeCoreOptions } from './TsRuntimeCore';
package/dist/core.cjs CHANGED
@@ -187,526 +187,18 @@ function numericArrayToNumbers(values) {
187
187
  return Array.from(values);
188
188
  }
189
189
 
190
- // src/core/compositeAxis.ts
191
- function toAUList(value) {
192
- if (value === void 0) return [];
193
- return Array.isArray(value) ? value : [value];
194
- }
195
- function findBoneBindingForNode(auToBones, auId, nodeKey) {
196
- return auToBones[auId]?.find((candidate) => candidate.node === nodeKey) ?? null;
197
- }
198
- function getCompositeAxisValue(axisConfig, getValue) {
199
- if (!axisConfig) return 0;
200
- const negativeAUs = toAUList(axisConfig.negative);
201
- const positiveAUs = toAUList(axisConfig.positive);
202
- if (negativeAUs.length > 0 && positiveAUs.length > 0) {
203
- const negativeValue = Math.max(...negativeAUs.map(getValue), 0);
204
- const positiveValue = Math.max(...positiveAUs.map(getValue), 0);
205
- return positiveValue - negativeValue;
206
- }
207
- if (axisConfig.aus.length > 1) {
208
- return Math.max(...axisConfig.aus.map(getValue), 0);
209
- }
210
- return getValue(axisConfig.aus[0]);
211
- }
212
- function getCompositeAxisBinding(nodeKey, axisConfig, direction, getValue, auToBones) {
213
- if (!axisConfig) return null;
214
- const directionalAUs = direction < 0 ? toAUList(axisConfig.negative) : toAUList(axisConfig.positive);
215
- const candidates = directionalAUs.length > 0 ? directionalAUs : axisConfig.aus;
216
- const ranked = [...candidates].sort((a, b) => getValue(b) - getValue(a));
217
- for (const auId of ranked) {
218
- const binding = findBoneBindingForNode(auToBones, auId, nodeKey);
219
- if (binding) return binding;
220
- }
221
- return null;
222
- }
223
-
224
- // src/mappings/visemeSystem.ts
225
- function hasOwn(value, key) {
226
- return Boolean(value && Object.prototype.hasOwnProperty.call(value, key));
227
- }
228
- function toSlotId(label, index) {
229
- const normalized = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
230
- return normalized || `viseme-${index}`;
231
- }
232
- function normalizeBindingWeight(weight) {
233
- return Number.isFinite(weight) ? Math.max(0, weight ?? 1) : 1;
234
- }
235
- function getProfileVisemeSlots(profile) {
236
- if (profile.visemeSlots && profile.visemeSlots.length > 0) {
237
- return [...profile.visemeSlots].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
238
- }
239
- return (profile.visemeKeys || []).map((key, index) => {
240
- const label = typeof key === "string" && key ? key : `Viseme ${index}`;
241
- return {
242
- id: toSlotId(label, index),
243
- label,
244
- order: index,
245
- defaultJawAmount: profile.visemeJawAmounts?.[index]
246
- };
247
- });
248
- }
249
- function getVisemeSlotIndex(profile, slotId) {
250
- return getProfileVisemeSlots(profile).findIndex((slot) => slot.id === slotId);
251
- }
252
- function getVisemeBindingTargets(profile, visemeIndex) {
253
- const slots = getProfileVisemeSlots(profile);
254
- const slot = slots[visemeIndex];
255
- const binding = slot ? profile.visemeBindings?.[slot.id] : void 0;
256
- const boundTargets = binding?.targets?.filter((target) => target.morph !== void 0 && target.morph !== "").map((target) => ({
257
- morph: target.morph,
258
- weight: normalizeBindingWeight(target.weight)
259
- }));
260
- if (boundTargets && boundTargets.length > 0) {
261
- return boundTargets;
262
- }
263
- if (binding?.morph !== void 0 && binding.morph !== "") {
264
- return [{ morph: binding.morph, weight: 1 }];
265
- }
266
- const legacyTarget = profile.visemeKeys?.[visemeIndex];
267
- return legacyTarget !== void 0 && legacyTarget !== "" ? [{ morph: legacyTarget, weight: 1 }] : [];
268
- }
269
- function getVisemeJawAmounts(profile) {
270
- const slots = getProfileVisemeSlots(profile);
271
- if (slots.length === 0) return profile.visemeJawAmounts ? [...profile.visemeJawAmounts] : void 0;
272
- return slots.map((slot, index) => slot.defaultJawAmount ?? profile.visemeJawAmounts?.[index] ?? 0);
273
- }
274
- function resolveVisemeMeshCategory(profile) {
275
- const morphToMesh = profile.morphToMesh || {};
276
- if (profile.visemeMeshCategory) return profile.visemeMeshCategory;
277
- if (hasOwn(morphToMesh, "viseme")) return "viseme";
278
- return "face";
279
- }
280
- function getMeshNamesForVisemeProfile(profile) {
281
- const morphToMesh = profile.morphToMesh || {};
282
- const category = resolveVisemeMeshCategory(profile);
283
- if (hasOwn(morphToMesh, category)) {
284
- return Array.isArray(morphToMesh[category]) ? [...morphToMesh[category]] : [];
285
- }
286
- return profile.visemeMeshCategory ? [] : [...morphToMesh.face || []];
287
- }
288
- function getMeshNamesForAUProfile(profile, auId) {
289
- const morphToMesh = profile.morphToMesh || {};
290
- const facePart = profile.auInfo?.[String(auId)]?.facePart;
291
- const category = facePart ? profile.auFacePartToMeshCategory?.[facePart] : void 0;
292
- if (category) return Array.isArray(morphToMesh[category]) ? [...morphToMesh[category]] : [];
293
- return [...morphToMesh.face || []];
294
- }
295
-
296
- // src/core/TsRuntimeCore.ts
297
- var identityQuat = { x: 0, y: 0, z: 0, w: 1 };
298
- function isRotationBoneBinding(binding) {
299
- return binding?.channel === "rx" || binding?.channel === "ry" || binding?.channel === "rz";
300
- }
301
- var TsRuntimeCore = class {
302
- constructor(options) {
303
- __publicField(this, "profile");
304
- __publicField(this, "model");
305
- __publicField(this, "auValues", {});
306
- __publicField(this, "auBalances", {});
307
- __publicField(this, "visemeValues");
308
- __publicField(this, "visemeJawScales");
309
- __publicField(this, "mixWeights");
310
- this.profile = options.profile;
311
- this.model = options.model;
312
- this.visemeValues = new Array(getProfileVisemeSlots(this.profile).length).fill(0);
313
- this.visemeJawScales = new Array(this.visemeValues.length).fill(1);
314
- this.mixWeights = { ...this.profile.auMixDefaults };
315
- }
316
- setProfile(profile) {
317
- this.profile = profile;
318
- this.visemeValues = new Array(getProfileVisemeSlots(this.profile).length).fill(0);
319
- this.visemeJawScales = new Array(this.visemeValues.length).fill(1);
320
- this.mixWeights = { ...this.profile.auMixDefaults };
321
- }
322
- setModelDescriptor(model) {
323
- this.model = model;
324
- }
325
- setAU(id, value, balance) {
326
- if (value < 0 && this.profile.continuumPairs) {
327
- const pairInfo = this.profile.continuumPairs[id];
328
- if (pairInfo) {
329
- const negAU = pairInfo.isNegative ? id : pairInfo.pairId;
330
- const posAU = pairInfo.isNegative ? pairInfo.pairId : id;
331
- this.setContinuum(negAU, posAU, pairInfo.isNegative ? -value : value, balance);
332
- return;
333
- }
334
- }
335
- this.auValues[id] = clamp01(value);
336
- if (balance !== void 0) {
337
- this.auBalances[id] = clampBalance(balance);
338
- }
339
- }
340
- getAU(id) {
341
- return this.auValues[id] ?? 0;
342
- }
343
- setContinuum(negAU, posAU, continuumValue, balance) {
344
- const value = Math.max(-1, Math.min(1, continuumValue));
345
- if (value < 0) {
346
- this.setAU(posAU, 0, balance);
347
- this.setAU(negAU, Math.abs(value), balance);
348
- return;
349
- }
350
- if (value > 0) {
351
- this.setAU(negAU, 0, balance);
352
- this.setAU(posAU, value, balance);
353
- return;
354
- }
355
- this.setAU(negAU, 0, balance);
356
- this.setAU(posAU, 0, balance);
357
- }
358
- setViseme(visemeIndex, value, jawScale = 1) {
359
- if (visemeIndex < 0 || visemeIndex >= this.visemeValues.length) return;
360
- this.visemeValues[visemeIndex] = clamp01(value);
361
- this.visemeJawScales[visemeIndex] = jawScale;
362
- }
363
- setVisemeById(slotId, value, jawScale = 1) {
364
- const index = getVisemeSlotIndex(this.profile, slotId);
365
- if (index < 0) return;
366
- this.setViseme(index, value, jawScale);
367
- }
368
- setAUMixWeight(id, weight) {
369
- this.mixWeights[id] = clamp01(weight);
370
- }
371
- evaluateFrameDelta(deltaSeconds) {
372
- const morphWrites = /* @__PURE__ */ new Map();
373
- this.collectAUWrites(morphWrites);
374
- this.collectVisemeWrites(morphWrites);
375
- return {
376
- deltaSeconds,
377
- morphTargets: Array.from(morphWrites.values()),
378
- bones: this.collectBoneWrites()
379
- };
380
- }
381
- collectAUWrites(morphWrites) {
382
- for (const [auIdText, entry] of Object.entries(this.profile.auToMorphs || {})) {
383
- const auId = Number(auIdText);
384
- if (Number.isNaN(auId) || !entry) continue;
385
- const mixWeight = this.isMixedAU(auId) ? this.getAUMixWeight(auId) : 1;
386
- const base = clamp01(this.auValues[auId] ?? 0) * mixWeight;
387
- const { left, right } = computeSideValues(base, this.auBalances[auId]);
388
- const meshNames = getMeshNamesForAUProfile(this.profile, auId);
389
- for (const morph of entry.left || []) {
390
- this.addMorphWrites(morphWrites, morph, meshNames, left);
391
- }
392
- for (const morph of entry.right || []) {
393
- this.addMorphWrites(morphWrites, morph, meshNames, right);
394
- }
395
- for (const morph of entry.center || []) {
396
- this.addMorphWrites(morphWrites, morph, meshNames, base);
397
- }
398
- }
399
- }
400
- collectVisemeWrites(morphWrites) {
401
- const meshNames = getMeshNamesForVisemeProfile(this.profile);
402
- const localMax = /* @__PURE__ */ new Map();
403
- for (let index = 0; index < this.visemeValues.length; index += 1) {
404
- const value = clamp01(this.visemeValues[index] ?? 0);
405
- for (const bindingTarget of getVisemeBindingTargets(this.profile, index)) {
406
- const weighted = clamp01(value * bindingTarget.weight);
407
- for (const resolved of this.resolveMorphTargets(bindingTarget.morph, meshNames)) {
408
- const key = morphWriteKey(resolved.mesh.id, resolved.morphTarget.id);
409
- const existing = localMax.get(key);
410
- if (!existing || weighted > existing.value) {
411
- localMax.set(key, {
412
- meshId: resolved.mesh.id,
413
- morphTargetId: resolved.morphTarget.id,
414
- value: weighted,
415
- mode: "absolute"
416
- });
417
- }
418
- }
419
- }
420
- }
421
- for (const [key, write] of localMax) {
422
- const existing = morphWrites.get(key);
423
- if (!existing || write.value > existing.value) {
424
- morphWrites.set(key, write);
425
- }
426
- }
427
- }
428
- collectBoneWrites() {
429
- const writes = [];
430
- const rotations = this.computeRotationState();
431
- for (const composite of this.profile.compositeRotations || []) {
432
- const bone = this.findBoneDescriptor(composite.node);
433
- if (!bone) continue;
434
- const transform = this.evaluateCompositeTransform(bone, composite, rotations[composite.node]);
435
- if (transform) {
436
- writes.push({
437
- boneId: bone.id,
438
- transform,
439
- mode: "absolute",
440
- space: "local"
441
- });
442
- }
443
- }
444
- for (const [nodeKey, translation] of Object.entries(this.computeTranslations())) {
445
- const bone = this.findBoneDescriptor(nodeKey);
446
- if (!bone) continue;
447
- const position = addVec3(bone.restTransform?.position, translation);
448
- upsertBoneWrite(writes, bone.id, { position });
449
- }
450
- const jawAmount = this.getActiveVisemeJawAmount();
451
- const jawBinding = this.findAutoVisemeJawBinding();
452
- const jawBone = jawBinding ? this.findBoneDescriptor(jawBinding.node) : void 0;
453
- if (jawBone && jawBinding?.maxDegrees && jawBinding.channel) {
454
- const current = writes.find((write) => write.boneId === jawBone.id);
455
- if (jawAmount > 1e-6 || !current?.transform.rotation) {
456
- const rest = jawBone.restTransform?.rotation ?? identityQuat;
457
- const rotation = jawAmount > 1e-6 ? multiplyQuat(
458
- rest,
459
- quatFromChannel(jawBinding.channel, deg2rad(jawBinding.maxDegrees) * jawAmount * jawBinding.scale)
460
- ) : rest;
461
- upsertBoneWrite(writes, jawBone.id, { rotation });
462
- }
463
- }
464
- return writes;
465
- }
466
- computeRotationState() {
467
- const rotations = {};
468
- for (const composite of this.profile.compositeRotations || []) {
469
- rotations[composite.node] = {
470
- pitch: this.getCompositeAxisValueForNode(composite.node, composite.pitch),
471
- yaw: this.getCompositeAxisValueForNode(composite.node, composite.yaw),
472
- roll: this.getCompositeAxisValueForNode(composite.node, composite.roll)
473
- };
474
- }
475
- return rotations;
476
- }
477
- evaluateCompositeTransform(bone, composite, state) {
478
- if (!state) return null;
479
- let rotation = bone.restTransform?.rotation ?? identityQuat;
480
- const applyAxis = (axisConfig, direction) => {
481
- if (!axisConfig || Math.abs(direction) <= 1e-6) return;
482
- const binding = getCompositeAxisBinding(
483
- composite.node,
484
- axisConfig,
485
- direction,
486
- (auId) => this.getEffectiveBoneAUValue(auId, composite.node),
487
- this.profile.auToBones
488
- );
489
- if (!binding?.maxDegrees || !binding.channel) return;
490
- rotation = multiplyQuat(
491
- rotation,
492
- quatFromChannel(binding.channel, deg2rad(binding.maxDegrees) * Math.abs(direction) * binding.scale)
493
- );
494
- };
495
- applyAxis(composite.yaw, state.yaw);
496
- applyAxis(composite.pitch, state.pitch);
497
- applyAxis(composite.roll, state.roll);
498
- return { rotation };
499
- }
500
- computeTranslations() {
501
- const translations = {};
502
- for (const [auIdText, bindings] of Object.entries(this.profile.auToBones || {})) {
503
- const auId = Number(auIdText);
504
- if (Number.isNaN(auId)) continue;
505
- const value = clamp01(this.auValues[auId] ?? 0);
506
- for (const binding of bindings || []) {
507
- if (binding.channel !== "tx" && binding.channel !== "ty" && binding.channel !== "tz") continue;
508
- if (binding.maxUnits === void 0) continue;
509
- const current = translations[binding.node] ?? { x: 0, y: 0, z: 0 };
510
- translations[binding.node] = current;
511
- if (value <= 1e-6) continue;
512
- const offset = clampSigned(value * binding.scale) * binding.maxUnits;
513
- translations[binding.node] = {
514
- x: binding.channel === "tx" ? offset : current.x,
515
- y: binding.channel === "ty" ? offset : current.y,
516
- z: binding.channel === "tz" ? offset : current.z
517
- };
518
- }
519
- }
520
- return translations;
521
- }
522
- addMorphWrites(morphWrites, morph, meshNames, value) {
523
- for (const resolved of this.resolveMorphTargets(morph, meshNames)) {
524
- const key = morphWriteKey(resolved.mesh.id, resolved.morphTarget.id);
525
- const existing = morphWrites.get(key);
526
- if (existing && existing.value >= value) continue;
527
- morphWrites.set(key, {
528
- meshId: resolved.mesh.id,
529
- morphTargetId: resolved.morphTarget.id,
530
- value,
531
- mode: "absolute"
532
- });
533
- }
534
- }
535
- resolveMorphTargets(morph, meshNames) {
536
- const result = [];
537
- for (const meshName of meshNames) {
538
- const mesh = this.model.meshes.find((candidate) => candidate.name === meshName);
539
- if (!mesh) continue;
540
- const morphTargets = mesh.morphTargetIds.map((id) => this.model.morphTargets.find((candidate) => candidate.id === id)).filter((target2) => !!target2);
541
- const target = typeof morph === "number" ? morphTargets.find((candidate) => candidate.hostIndex === morph) : resolveMorphTargetByName(morphTargets, morph, this.profile);
542
- if (target) {
543
- result.push({ mesh, morphTarget: target });
544
- }
545
- }
546
- return result;
547
- }
548
- findBoneDescriptor(nodeKey) {
549
- const configuredName = this.profile.boneNodes[nodeKey] || nodeKey;
550
- const prefix = this.profile.bonePrefix ?? "";
551
- const suffix = this.profile.boneSuffix ?? "";
552
- const prefixedName = prefix && !configuredName.startsWith(prefix) ? `${prefix}${configuredName}` : configuredName;
553
- const fullName = suffix && !prefixedName.endsWith(suffix) ? `${prefixedName}${suffix}` : prefixedName;
554
- const candidates = /* @__PURE__ */ new Set([nodeKey, configuredName, fullName]);
555
- return this.model.bones.find((bone) => candidates.has(bone.name));
556
- }
557
- findAutoVisemeJawBinding() {
558
- const candidates = [
559
- this.profile.auToBones[103]?.find(isRotationBoneBinding),
560
- this.profile.auToBones[26]?.find(isRotationBoneBinding)
561
- ].filter((binding) => !!binding);
562
- return candidates.find((binding) => !!this.findBoneDescriptor(binding.node)) ?? candidates[0] ?? null;
563
- }
564
- getActiveVisemeJawAmount() {
565
- const jawAmounts = getVisemeJawAmounts(this.profile) ?? [];
566
- let jawAmount = 0;
567
- for (let index = 0; index < this.visemeValues.length; index += 1) {
568
- const value = clamp01(this.visemeValues[index] ?? 0);
569
- if (value <= 1e-6) continue;
570
- const jawScale = this.visemeJawScales[index] ?? 1;
571
- if (Math.abs(jawScale) <= 1e-6) continue;
572
- jawAmount = Math.max(jawAmount, (jawAmounts[index] ?? 0) * value * jawScale);
573
- }
574
- return jawAmount;
575
- }
576
- getCompositeAxisValueForNode(nodeKey, axisConfig) {
577
- return getCompositeAxisValue(axisConfig, (auId) => this.getEffectiveBoneAUValue(auId, nodeKey));
578
- }
579
- getEffectiveBoneAUValue(auId, nodeKey) {
580
- const rawValue = clamp01(this.auValues[auId] ?? 0);
581
- if (rawValue <= 1e-6) return 0;
582
- const binding = this.profile.auToBones[auId]?.find((candidate) => candidate.node === nodeKey) ?? null;
583
- if (!binding?.side) return rawValue;
584
- return rawValue * getSideScale(this.auBalances[auId] ?? 0, binding.side);
585
- }
586
- getAUMixWeight(auId) {
587
- return this.mixWeights[auId] ?? this.profile.auMixDefaults?.[auId] ?? 1;
588
- }
589
- isMixedAU(auId) {
590
- const morphs = this.profile.auToMorphs[auId];
591
- const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
592
- return !!(hasMorphs && this.profile.auToBones[auId]?.length);
593
- }
594
- };
595
- function resolveMorphTargetByName(targets, key, profile) {
596
- const prefix = profile.morphPrefix || "";
597
- const suffix = profile.morphSuffix || "";
598
- const fullName = prefix + key + suffix;
599
- const exact = targets.find((target) => target.name === fullName);
600
- if (exact) return exact;
601
- const suffixRegex = profile.suffixPattern ? new RegExp(profile.suffixPattern) : null;
602
- if (!suffixRegex) return void 0;
603
- return targets.find((target) => {
604
- if (!target.name.startsWith(fullName)) return false;
605
- const candidateSuffix = target.name.slice(fullName.length);
606
- return candidateSuffix === "" || suffixRegex.test(candidateSuffix);
607
- });
608
- }
609
- function computeSideValues(base, balance) {
610
- const b = clampBalance(balance ?? 0);
611
- if (b === 0) return { left: base, right: base };
612
- if (b < 0) return { left: base, right: base * (1 + b) };
613
- return { left: base * (1 - b), right: base };
614
- }
615
- function getSideScale(balance, side) {
616
- if (side !== "left" && side !== "right") return 1;
617
- const b = clampBalance(balance);
618
- if (side === "left") return b > 0 ? 1 - b : 1;
619
- return b < 0 ? 1 + b : 1;
620
- }
621
- function addVec3(base, offset) {
622
- return {
623
- x: (base?.x ?? 0) + offset.x,
624
- y: (base?.y ?? 0) + offset.y,
625
- z: (base?.z ?? 0) + offset.z
626
- };
627
- }
628
- function upsertBoneWrite(writes, boneId, transform) {
629
- const existingIndex = writes.findIndex((write) => write.boneId === boneId);
630
- if (existingIndex >= 0) {
631
- const existing = writes[existingIndex];
632
- writes[existingIndex] = {
633
- ...existing,
634
- transform: {
635
- ...existing.transform,
636
- ...transform
637
- }
638
- };
639
- return;
640
- }
641
- writes.push({
642
- boneId,
643
- transform,
644
- mode: "absolute",
645
- space: "local"
646
- });
647
- }
648
- function quatFromChannel(channel, radians) {
649
- if (channel === "rx") return quatFromAxis({ x: 1, y: 0, z: 0 }, radians);
650
- if (channel === "ry") return quatFromAxis({ x: 0, y: 1, z: 0 }, radians);
651
- return quatFromAxis({ x: 0, y: 0, z: 1 }, radians);
652
- }
653
- function quatFromAxis(axis, radians) {
654
- const half = radians / 2;
655
- const s = Math.sin(half);
656
- return normalizeQuat({
657
- x: axis.x * s,
658
- y: axis.y * s,
659
- z: axis.z * s,
660
- w: Math.cos(half)
661
- });
662
- }
663
- function multiplyQuat(a, b) {
664
- return normalizeQuat({
665
- x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
666
- y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
667
- z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
668
- w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
669
- });
670
- }
671
- function normalizeQuat(q) {
672
- const len = Math.hypot(q.x, q.y, q.z, q.w);
673
- if (len <= 1e-12) return identityQuat;
674
- return {
675
- x: q.x / len,
676
- y: q.y / len,
677
- z: q.z / len,
678
- w: q.w / len
679
- };
680
- }
681
- function deg2rad(degrees) {
682
- return degrees * Math.PI / 180;
683
- }
684
- function clamp01(value) {
685
- if (!Number.isFinite(value)) return 0;
686
- return value < 0 ? 0 : value > 1 ? 1 : value;
687
- }
688
- function clampSigned(value) {
689
- if (!Number.isFinite(value)) return 0;
690
- return value < -1 ? -1 : value > 1 ? 1 : value;
691
- }
692
- function clampBalance(value) {
693
- if (!Number.isFinite(value)) return 0;
694
- return Math.max(-1, Math.min(1, value));
695
- }
696
- function morphWriteKey(meshId, morphTargetId) {
697
- return `${meshId}:${morphTargetId}`;
698
- }
699
-
700
190
  // src/core/WasmRuntimeCore.ts
701
191
  var WasmRuntimeCore = class {
702
192
  constructor(options) {
703
193
  __publicField(this, "wasm");
704
194
  __publicField(this, "core");
195
+ __publicField(this, "allowEmptyBindings");
705
196
  __publicField(this, "profile");
706
197
  __publicField(this, "model");
707
198
  this.wasm = options.wasm;
708
199
  this.profile = options.profile;
709
200
  this.model = options.model;
201
+ this.allowEmptyBindings = options.allowEmptyBindings ?? false;
710
202
  const RuntimeCtor = options.wasm.RuntimeCore;
711
203
  if (!RuntimeCtor) {
712
204
  throw new Error("Embody Wasm module does not export RuntimeCore");
@@ -725,6 +217,24 @@ var WasmRuntimeCore = class {
725
217
  setAU(id, value, balance = 0) {
726
218
  this.core.set_au(id >>> 0, value, balance);
727
219
  }
220
+ setContinuum(negativeAU, positiveAU, value, balance = 0) {
221
+ this.core.set_continuum(negativeAU >>> 0, positiveAU >>> 0, value, balance);
222
+ }
223
+ transitionAU(id, to, durationMs, balance = Number.NaN) {
224
+ this.core.transition_au(id >>> 0, to, durationMs, balance);
225
+ }
226
+ transitionViseme(visemeIndex, to, durationMs, jawScale = 1) {
227
+ this.core.transition_viseme(visemeIndex >>> 0, to, durationMs, jawScale);
228
+ }
229
+ update(deltaSeconds) {
230
+ return this.core.update(deltaSeconds);
231
+ }
232
+ activeTransitionCount() {
233
+ return this.core.active_transition_count();
234
+ }
235
+ clearTransitions() {
236
+ this.core.clear_transitions();
237
+ }
728
238
  getAU(id) {
729
239
  return this.core.get_au(id >>> 0);
730
240
  }
@@ -760,7 +270,13 @@ var WasmRuntimeCore = class {
760
270
  };
761
271
  }
762
272
  reloadBindings() {
763
- this.core.configure_with_profile(JSON.stringify(this.profile), JSON.stringify(this.model));
273
+ const profileJson = JSON.stringify(this.profile);
274
+ const modelJson = JSON.stringify(this.model);
275
+ if (this.allowEmptyBindings) {
276
+ this.core.configure(profileJson, modelJson);
277
+ } else {
278
+ this.core.configure_with_profile(profileJson, modelJson);
279
+ }
764
280
  }
765
281
  };
766
282
  function unpackBoneFrameDelta(values) {
@@ -816,7 +332,6 @@ exports.TEMPLATE_SKELETON_FIT_TRANSFORM_STRIDE = TEMPLATE_SKELETON_FIT_TRANSFORM
816
332
  exports.TEMPLATE_SKELETON_FIT_VERTICAL_ANCHORS = TEMPLATE_SKELETON_FIT_VERTICAL_ANCHORS;
817
333
  exports.TEMPLATE_SKELETON_FIT_VERTICAL_AXES = TEMPLATE_SKELETON_FIT_VERTICAL_AXES;
818
334
  exports.TsClipCompiler = TsClipCompiler;
819
- exports.TsRuntimeCore = TsRuntimeCore;
820
335
  exports.WasmRuntimeCore = WasmRuntimeCore;
821
336
  exports.composeTemplateSkeletonFitTransform = composeTemplateSkeletonFitTransform;
822
337
  exports.isTemplateSkeletonFitStatus = isTemplateSkeletonFitStatus;