@lovelace_lol/loom3 1.0.36 → 1.0.37

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/dist/index.d.cts CHANGED
@@ -236,10 +236,12 @@ type PresetType = 'cc4' | 'skeletal' | 'fish' | 'custom';
236
236
  * This allows frontend to pass a string instead of importing the full preset.
237
237
  */
238
238
  declare function getPreset(presetType: PresetType | string | undefined): Profile;
239
+ declare const resolvePreset: typeof getPreset;
239
240
  /**
240
241
  * Get a preset, then extend it with an optional profile.
241
242
  */
242
243
  declare function getPresetWithProfile(presetType: PresetType | string | undefined, profile?: Partial<Profile>): Profile;
244
+ declare const resolvePresetWithOverrides: typeof getPresetWithProfile;
243
245
 
244
246
  /**
245
247
  * Loom3 - Core Type Definitions
@@ -452,6 +454,34 @@ interface AnimationActionHandle {
452
454
  /** Promise that resolves when animation completes (only for non-looping) */
453
455
  finished: Promise<void>;
454
456
  }
457
+ type ClipEvent = {
458
+ type: 'keyframe';
459
+ clipName: string;
460
+ keyframeIndex: number;
461
+ totalKeyframes: number;
462
+ currentTime: number;
463
+ duration: number;
464
+ iteration: number;
465
+ } | {
466
+ type: 'loop';
467
+ clipName: string;
468
+ iteration: number;
469
+ currentTime: number;
470
+ duration: number;
471
+ } | {
472
+ type: 'seek';
473
+ clipName: string;
474
+ currentTime: number;
475
+ duration: number;
476
+ iteration: number;
477
+ } | {
478
+ type: 'completed';
479
+ clipName: string;
480
+ currentTime: number;
481
+ duration: number;
482
+ iteration: number;
483
+ };
484
+ type ClipEventListener = (event: ClipEvent) => void;
455
485
  /**
456
486
  * A single keyframe point in an animation curve.
457
487
  */
@@ -551,6 +581,8 @@ interface ClipHandle {
551
581
  getTime: () => number;
552
582
  /** Get total clip duration in seconds */
553
583
  getDuration: () => number;
584
+ /** Subscribe to clip lifecycle events emitted by the runtime update loop */
585
+ subscribe?: (listener: ClipEventListener) => () => void;
554
586
  /** Promise that resolves when clip finishes (non-looping only) */
555
587
  finished: Promise<void>;
556
588
  }
@@ -2414,4 +2446,4 @@ declare function detectFacingDirection(model: THREE.Object3D, eyeBoneNames?: {
2414
2446
  right: string[];
2415
2447
  }): 'forward' | 'backward' | 'unknown';
2416
2448
 
2417
- export { type AUInfo, type AUSelector, AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, type AnalyzeModelOptions, type Animation, type AnimationActionHandle, type AnimationAnalysis, type AnimationBlendMode, type AnimationClipInfo, type AnimationEasing, type AnimationInfo, type AnimationPlayOptions, type AnimationSource, type AnimationState, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, type BlendingMode, type BoneBinding, type BoneInfo, type BoneKey, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, type CharacterConfig, type CharacterRegistry, type ClipHandle, type ClipOptions, type CompositeRotation, type CompositeRotationState, type CurvePoint, type CurvesMap, DEFAULT_HAIR_PHYSICS_CONFIG, type ExpandAnimation, type ExpandedRegionState, FISH_AU_MAPPING_CONFIG, type FaceCenterResult, type FallbackConfig, type FindFaceCenterOptions, type Hair, type HairMorphAxis, type HairMorphOutput$1 as HairMorphOutput, type HairMorphTargetMapping, type HairMorphTargetValueMapping, type HairMorphTargetsConfig, type HairObjectRef, type HairObjectState, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysicsDirectionConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsProfileConfig, type HairPhysicsRuntimeConfig, type HairPhysicsRuntimeConfigUpdate, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LineConfig, type LineCurve, type LineStyle, Loom3, type Loom3Config, Loom3 as Loom3Three, type LoomLarge, type LoomLargeConfig, Loom3 as LoomLargeThree, MORPH_TO_MESH, type MappingConsistencyResult, type MappingCorrection, type MappingCorrectionOptions, type MappingCorrectionResult, type MappingIssue, type MarkerGroup, type MarkerStyle, type MarkerStyleOverrides, type MeshCategory, type MeshInfo, type MeshMaterialSettings, type MixerLoopMode, type ModelAnalysisReport, type ModelData, type ModelMeshInfo, type MorphCategory, type MorphInfo, type MorphTargetRef, type MorphTargetsBySide, type NamedDirection, type PresetType, type Profile, type ReadyPayload, type Region, type RotationAxis, type RotationsState, type Snippet, type TrackInfo, type TransitionHandle, VISEME_JAW_AMOUNTS, VISEME_KEYS, type ValidateMappingOptions, type ValidationResult, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, suggestBestPreset, validateMappingConfig, validateMappings };
2449
+ export { type AUInfo, type AUSelector, AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, type AnalyzeModelOptions, type Animation, type AnimationActionHandle, type AnimationAnalysis, type AnimationBlendMode, type AnimationClipInfo, type AnimationEasing, type AnimationInfo, type AnimationPlayOptions, type AnimationSource, type AnimationState, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, type BlendingMode, type BoneBinding, type BoneInfo, type BoneKey, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, type CharacterConfig, type CharacterRegistry, type ClipEvent, type ClipEventListener, type ClipHandle, type ClipOptions, type CompositeRotation, type CompositeRotationState, type CurvePoint, type CurvesMap, DEFAULT_HAIR_PHYSICS_CONFIG, type ExpandAnimation, type ExpandedRegionState, FISH_AU_MAPPING_CONFIG, type FaceCenterResult, type FallbackConfig, type FindFaceCenterOptions, type Hair, type HairMorphAxis, type HairMorphOutput$1 as HairMorphOutput, type HairMorphTargetMapping, type HairMorphTargetValueMapping, type HairMorphTargetsConfig, type HairObjectRef, type HairObjectState, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysicsDirectionConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsProfileConfig, type HairPhysicsRuntimeConfig, type HairPhysicsRuntimeConfigUpdate, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LineConfig, type LineCurve, type LineStyle, Loom3, type Loom3Config, Loom3 as Loom3Three, type LoomLarge, type LoomLargeConfig, Loom3 as LoomLargeThree, MORPH_TO_MESH, type MappingConsistencyResult, type MappingCorrection, type MappingCorrectionOptions, type MappingCorrectionResult, type MappingIssue, type MarkerGroup, type MarkerStyle, type MarkerStyleOverrides, type MeshCategory, type MeshInfo, type MeshMaterialSettings, type MixerLoopMode, type ModelAnalysisReport, type ModelData, type ModelMeshInfo, type MorphCategory, type MorphInfo, type MorphTargetRef, type MorphTargetsBySide, type NamedDirection, type PresetType, type Profile, type ReadyPayload, type Region, type RotationAxis, type RotationsState, type Snippet, type TrackInfo, type TransitionHandle, VISEME_JAW_AMOUNTS, VISEME_KEYS, type ValidateMappingOptions, type ValidationResult, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, resolvePreset, resolvePresetWithOverrides, suggestBestPreset, validateMappingConfig, validateMappings };
package/dist/index.d.ts CHANGED
@@ -236,10 +236,12 @@ type PresetType = 'cc4' | 'skeletal' | 'fish' | 'custom';
236
236
  * This allows frontend to pass a string instead of importing the full preset.
237
237
  */
238
238
  declare function getPreset(presetType: PresetType | string | undefined): Profile;
239
+ declare const resolvePreset: typeof getPreset;
239
240
  /**
240
241
  * Get a preset, then extend it with an optional profile.
241
242
  */
242
243
  declare function getPresetWithProfile(presetType: PresetType | string | undefined, profile?: Partial<Profile>): Profile;
244
+ declare const resolvePresetWithOverrides: typeof getPresetWithProfile;
243
245
 
244
246
  /**
245
247
  * Loom3 - Core Type Definitions
@@ -452,6 +454,34 @@ interface AnimationActionHandle {
452
454
  /** Promise that resolves when animation completes (only for non-looping) */
453
455
  finished: Promise<void>;
454
456
  }
457
+ type ClipEvent = {
458
+ type: 'keyframe';
459
+ clipName: string;
460
+ keyframeIndex: number;
461
+ totalKeyframes: number;
462
+ currentTime: number;
463
+ duration: number;
464
+ iteration: number;
465
+ } | {
466
+ type: 'loop';
467
+ clipName: string;
468
+ iteration: number;
469
+ currentTime: number;
470
+ duration: number;
471
+ } | {
472
+ type: 'seek';
473
+ clipName: string;
474
+ currentTime: number;
475
+ duration: number;
476
+ iteration: number;
477
+ } | {
478
+ type: 'completed';
479
+ clipName: string;
480
+ currentTime: number;
481
+ duration: number;
482
+ iteration: number;
483
+ };
484
+ type ClipEventListener = (event: ClipEvent) => void;
455
485
  /**
456
486
  * A single keyframe point in an animation curve.
457
487
  */
@@ -551,6 +581,8 @@ interface ClipHandle {
551
581
  getTime: () => number;
552
582
  /** Get total clip duration in seconds */
553
583
  getDuration: () => number;
584
+ /** Subscribe to clip lifecycle events emitted by the runtime update loop */
585
+ subscribe?: (listener: ClipEventListener) => () => void;
554
586
  /** Promise that resolves when clip finishes (non-looping only) */
555
587
  finished: Promise<void>;
556
588
  }
@@ -2414,4 +2446,4 @@ declare function detectFacingDirection(model: THREE.Object3D, eyeBoneNames?: {
2414
2446
  right: string[];
2415
2447
  }): 'forward' | 'backward' | 'unknown';
2416
2448
 
2417
- export { type AUInfo, type AUSelector, AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, type AnalyzeModelOptions, type Animation, type AnimationActionHandle, type AnimationAnalysis, type AnimationBlendMode, type AnimationClipInfo, type AnimationEasing, type AnimationInfo, type AnimationPlayOptions, type AnimationSource, type AnimationState, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, type BlendingMode, type BoneBinding, type BoneInfo, type BoneKey, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, type CharacterConfig, type CharacterRegistry, type ClipHandle, type ClipOptions, type CompositeRotation, type CompositeRotationState, type CurvePoint, type CurvesMap, DEFAULT_HAIR_PHYSICS_CONFIG, type ExpandAnimation, type ExpandedRegionState, FISH_AU_MAPPING_CONFIG, type FaceCenterResult, type FallbackConfig, type FindFaceCenterOptions, type Hair, type HairMorphAxis, type HairMorphOutput$1 as HairMorphOutput, type HairMorphTargetMapping, type HairMorphTargetValueMapping, type HairMorphTargetsConfig, type HairObjectRef, type HairObjectState, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysicsDirectionConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsProfileConfig, type HairPhysicsRuntimeConfig, type HairPhysicsRuntimeConfigUpdate, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LineConfig, type LineCurve, type LineStyle, Loom3, type Loom3Config, Loom3 as Loom3Three, type LoomLarge, type LoomLargeConfig, Loom3 as LoomLargeThree, MORPH_TO_MESH, type MappingConsistencyResult, type MappingCorrection, type MappingCorrectionOptions, type MappingCorrectionResult, type MappingIssue, type MarkerGroup, type MarkerStyle, type MarkerStyleOverrides, type MeshCategory, type MeshInfo, type MeshMaterialSettings, type MixerLoopMode, type ModelAnalysisReport, type ModelData, type ModelMeshInfo, type MorphCategory, type MorphInfo, type MorphTargetRef, type MorphTargetsBySide, type NamedDirection, type PresetType, type Profile, type ReadyPayload, type Region, type RotationAxis, type RotationsState, type Snippet, type TrackInfo, type TransitionHandle, VISEME_JAW_AMOUNTS, VISEME_KEYS, type ValidateMappingOptions, type ValidationResult, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, suggestBestPreset, validateMappingConfig, validateMappings };
2449
+ export { type AUInfo, type AUSelector, AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, type AnalyzeModelOptions, type Animation, type AnimationActionHandle, type AnimationAnalysis, type AnimationBlendMode, type AnimationClipInfo, type AnimationEasing, type AnimationInfo, type AnimationPlayOptions, type AnimationSource, type AnimationState, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, type BlendingMode, type BoneBinding, type BoneInfo, type BoneKey, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, type CharacterConfig, type CharacterRegistry, type ClipEvent, type ClipEventListener, type ClipHandle, type ClipOptions, type CompositeRotation, type CompositeRotationState, type CurvePoint, type CurvesMap, DEFAULT_HAIR_PHYSICS_CONFIG, type ExpandAnimation, type ExpandedRegionState, FISH_AU_MAPPING_CONFIG, type FaceCenterResult, type FallbackConfig, type FindFaceCenterOptions, type Hair, type HairMorphAxis, type HairMorphOutput$1 as HairMorphOutput, type HairMorphTargetMapping, type HairMorphTargetValueMapping, type HairMorphTargetsConfig, type HairObjectRef, type HairObjectState, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysicsDirectionConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsProfileConfig, type HairPhysicsRuntimeConfig, type HairPhysicsRuntimeConfigUpdate, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LineConfig, type LineCurve, type LineStyle, Loom3, type Loom3Config, Loom3 as Loom3Three, type LoomLarge, type LoomLargeConfig, Loom3 as LoomLargeThree, MORPH_TO_MESH, type MappingConsistencyResult, type MappingCorrection, type MappingCorrectionOptions, type MappingCorrectionResult, type MappingIssue, type MarkerGroup, type MarkerStyle, type MarkerStyleOverrides, type MeshCategory, type MeshInfo, type MeshMaterialSettings, type MixerLoopMode, type ModelAnalysisReport, type ModelData, type ModelMeshInfo, type MorphCategory, type MorphInfo, type MorphTargetRef, type MorphTargetsBySide, type NamedDirection, type PresetType, type Profile, type ReadyPayload, type Region, type RotationAxis, type RotationsState, type Snippet, type TrackInfo, type TransitionHandle, VISEME_JAW_AMOUNTS, VISEME_KEYS, type ValidateMappingOptions, type ValidationResult, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, resolvePreset, resolvePresetWithOverrides, suggestBestPreset, validateMappingConfig, validateMappings };
package/dist/index.js CHANGED
@@ -270,6 +270,8 @@ var makeActionId = () => `act_${Math.random().toString(36).slice(2, 8)}_${Date.n
270
270
  var X_AXIS = new Vector3(1, 0, 0);
271
271
  var Y_AXIS = new Vector3(0, 1, 0);
272
272
  var Z_AXIS = new Vector3(0, 0, 1);
273
+ var CLIP_EVENT_METADATA_KEY = "__loom3ClipEvents";
274
+ var CLIP_EVENT_EPSILON = 1e-4;
273
275
  var BakedAnimationController = class {
274
276
  constructor(host) {
275
277
  __publicField(this, "host");
@@ -288,6 +290,7 @@ var BakedAnimationController = class {
288
290
  __publicField(this, "playbackState", /* @__PURE__ */ new Map());
289
291
  __publicField(this, "actionIds", /* @__PURE__ */ new WeakMap());
290
292
  __publicField(this, "actionIdToClip", /* @__PURE__ */ new Map());
293
+ __publicField(this, "clipMonitors", /* @__PURE__ */ new Map());
291
294
  this.host = host;
292
295
  }
293
296
  getActionId(action) {
@@ -301,6 +304,161 @@ var BakedAnimationController = class {
301
304
  action.__actionId = actionId;
302
305
  return actionId;
303
306
  }
307
+ setClipEventMetadata(clip, metadata) {
308
+ const userData = clip.userData ?? (clip.userData = {});
309
+ userData[CLIP_EVENT_METADATA_KEY] = metadata;
310
+ }
311
+ getClipEventMetadata(clip) {
312
+ const userData = clip.userData;
313
+ const keyframeTimes = Array.isArray(userData?.[CLIP_EVENT_METADATA_KEY]?.keyframeTimes) ? userData[CLIP_EVENT_METADATA_KEY].keyframeTimes.filter((time) => Number.isFinite(time)) : [];
314
+ return { keyframeTimes };
315
+ }
316
+ getKeyframeIndex(times, currentTime) {
317
+ if (!times.length) return -1;
318
+ const target = Math.max(0, currentTime) + 1e-3;
319
+ let lo = 0;
320
+ let hi = times.length - 1;
321
+ let idx = 0;
322
+ while (lo <= hi) {
323
+ const mid = lo + hi >>> 1;
324
+ if (times[mid] <= target) {
325
+ idx = mid;
326
+ lo = mid + 1;
327
+ } else {
328
+ hi = mid - 1;
329
+ }
330
+ }
331
+ return idx;
332
+ }
333
+ emitClipEvent(monitor, event) {
334
+ for (const listener of Array.from(monitor.listeners)) {
335
+ try {
336
+ listener(event);
337
+ } catch (error) {
338
+ console.error("[Loom3] clip event listener failed", error);
339
+ }
340
+ }
341
+ }
342
+ emitKeyframesForRange(monitor, startTime, endTime, direction, includeStart) {
343
+ if (!monitor.keyframeTimes.length) return;
344
+ const times = direction === 1 ? monitor.keyframeTimes : [...monitor.keyframeTimes].reverse();
345
+ for (const time of times) {
346
+ const matchesForward = direction === 1 && (includeStart ? time >= startTime - CLIP_EVENT_EPSILON : time > startTime + CLIP_EVENT_EPSILON) && time <= endTime + CLIP_EVENT_EPSILON;
347
+ const matchesReverse = direction === -1 && (includeStart ? time <= startTime + CLIP_EVENT_EPSILON : time < startTime - CLIP_EVENT_EPSILON) && time >= endTime - CLIP_EVENT_EPSILON;
348
+ if (!matchesForward && !matchesReverse) continue;
349
+ const keyframeIndex = monitor.keyframeTimes.indexOf(time);
350
+ monitor.lastKeyframeIndex = keyframeIndex;
351
+ this.emitClipEvent(monitor, {
352
+ type: "keyframe",
353
+ clipName: monitor.clipName,
354
+ keyframeIndex,
355
+ totalKeyframes: monitor.keyframeTimes.length,
356
+ currentTime: time,
357
+ duration: monitor.duration,
358
+ iteration: monitor.iteration
359
+ });
360
+ }
361
+ }
362
+ resetClipMonitor(monitor, currentTime) {
363
+ monitor.iteration = 0;
364
+ monitor.direction = monitor.initialDirection;
365
+ monitor.lastTime = currentTime;
366
+ monitor.lastKeyframeIndex = this.getKeyframeIndex(monitor.keyframeTimes, currentTime);
367
+ monitor.finishedPending = false;
368
+ }
369
+ syncClipMonitorTime(monitor, currentTime, emitSeek = false) {
370
+ const clamped = Math.max(0, Math.min(monitor.duration, currentTime));
371
+ monitor.lastTime = clamped;
372
+ monitor.lastKeyframeIndex = this.getKeyframeIndex(monitor.keyframeTimes, clamped);
373
+ if (emitSeek) {
374
+ this.emitClipEvent(monitor, {
375
+ type: "seek",
376
+ clipName: monitor.clipName,
377
+ currentTime: clamped,
378
+ duration: monitor.duration,
379
+ iteration: monitor.iteration
380
+ });
381
+ }
382
+ }
383
+ cleanupClipMonitor(actionId) {
384
+ const monitor = this.clipMonitors.get(actionId);
385
+ if (!monitor || monitor.cleanedUp) return;
386
+ monitor.cleanedUp = true;
387
+ try {
388
+ monitor.action.paused = true;
389
+ } catch {
390
+ }
391
+ monitor.resolveFinished();
392
+ monitor.listeners.clear();
393
+ this.clipMonitors.delete(actionId);
394
+ this.actionIdToClip.delete(actionId);
395
+ }
396
+ advanceClipMonitor(monitor, previousTime) {
397
+ if (monitor.cleanedUp || monitor.action.paused && !monitor.finishedPending) return;
398
+ const currentTime = Math.max(0, Math.min(monitor.duration, monitor.action.time));
399
+ const delta = currentTime - previousTime;
400
+ if (monitor.loopMode === "pingpong") {
401
+ const movingForward = monitor.direction === 1;
402
+ const bouncedAtEnd = movingForward && delta < -CLIP_EVENT_EPSILON;
403
+ const bouncedAtStart = !movingForward && delta > CLIP_EVENT_EPSILON;
404
+ if (bouncedAtEnd) {
405
+ this.emitKeyframesForRange(monitor, previousTime, monitor.duration, 1, false);
406
+ monitor.direction = -1;
407
+ this.emitKeyframesForRange(monitor, monitor.duration, currentTime, -1, false);
408
+ } else if (bouncedAtStart) {
409
+ this.emitKeyframesForRange(monitor, previousTime, 0, -1, false);
410
+ monitor.direction = 1;
411
+ monitor.iteration += 1;
412
+ this.emitClipEvent(monitor, {
413
+ type: "loop",
414
+ clipName: monitor.clipName,
415
+ iteration: monitor.iteration,
416
+ currentTime: 0,
417
+ duration: monitor.duration
418
+ });
419
+ this.emitKeyframesForRange(monitor, 0, currentTime, 1, false);
420
+ } else if (delta > CLIP_EVENT_EPSILON) {
421
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, 1, false);
422
+ monitor.direction = 1;
423
+ } else if (delta < -CLIP_EVENT_EPSILON) {
424
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, -1, false);
425
+ monitor.direction = -1;
426
+ }
427
+ } else if (monitor.direction === 1) {
428
+ const wrapped = currentTime + CLIP_EVENT_EPSILON < previousTime;
429
+ if (wrapped) {
430
+ this.emitKeyframesForRange(monitor, previousTime, monitor.duration, 1, false);
431
+ monitor.iteration += 1;
432
+ this.emitClipEvent(monitor, {
433
+ type: "loop",
434
+ clipName: monitor.clipName,
435
+ iteration: monitor.iteration,
436
+ currentTime: 0,
437
+ duration: monitor.duration
438
+ });
439
+ this.emitKeyframesForRange(monitor, 0, currentTime, 1, true);
440
+ } else if (delta > CLIP_EVENT_EPSILON) {
441
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, 1, false);
442
+ }
443
+ } else {
444
+ const wrapped = currentTime > previousTime + CLIP_EVENT_EPSILON;
445
+ if (wrapped) {
446
+ this.emitKeyframesForRange(monitor, previousTime, 0, -1, false);
447
+ monitor.iteration += 1;
448
+ this.emitClipEvent(monitor, {
449
+ type: "loop",
450
+ clipName: monitor.clipName,
451
+ iteration: monitor.iteration,
452
+ currentTime: monitor.duration,
453
+ duration: monitor.duration
454
+ });
455
+ this.emitKeyframesForRange(monitor, monitor.duration, currentTime, -1, true);
456
+ } else if (delta < -CLIP_EVENT_EPSILON) {
457
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, -1, false);
458
+ }
459
+ }
460
+ this.syncClipMonitorTime(monitor, currentTime);
461
+ }
304
462
  normalizePlaybackOptions(options, defaults) {
305
463
  const clipOptions = options;
306
464
  const rawRate = options?.playbackRate ?? options?.speed ?? 1;
@@ -512,7 +670,28 @@ var BakedAnimationController = class {
512
670
  }
513
671
  update(dtSeconds) {
514
672
  if (this.animationMixer) {
673
+ const snapshots = Array.from(this.clipMonitors.values()).map((monitor) => ({
674
+ actionId: monitor.actionId,
675
+ previousTime: monitor.action.time
676
+ }));
515
677
  this.animationMixer.update(dtSeconds);
678
+ for (const { actionId, previousTime } of snapshots) {
679
+ const monitor = this.clipMonitors.get(actionId);
680
+ if (!monitor) continue;
681
+ this.advanceClipMonitor(monitor, previousTime);
682
+ if (monitor.finishedPending) {
683
+ const finalTime = Math.max(0, Math.min(monitor.duration, monitor.action.time));
684
+ this.syncClipMonitorTime(monitor, finalTime);
685
+ this.emitClipEvent(monitor, {
686
+ type: "completed",
687
+ clipName: monitor.clipName,
688
+ currentTime: finalTime,
689
+ duration: monitor.duration,
690
+ iteration: monitor.iteration
691
+ });
692
+ this.cleanupClipMonitor(actionId);
693
+ }
694
+ }
516
695
  }
517
696
  }
518
697
  dispose() {
@@ -532,6 +711,7 @@ var BakedAnimationController = class {
532
711
  this.clipHandles.clear();
533
712
  this.clipSources.clear();
534
713
  this.playbackState.clear();
714
+ this.clipMonitors.clear();
535
715
  }
536
716
  loadAnimationClips(clips) {
537
717
  const model = this.host.getModel();
@@ -670,6 +850,7 @@ var BakedAnimationController = class {
670
850
  }
671
851
  const action = this.animationActions.get(clipName);
672
852
  if (action) {
853
+ const actionId = this.getActionId(action);
673
854
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
674
855
  action.stop();
675
856
  if (!isBaked && this.animationMixer) {
@@ -692,9 +873,11 @@ var BakedAnimationController = class {
692
873
  }
693
874
  }
694
875
  this.animationFinishedCallbacks.delete(clipName);
876
+ if (actionId) this.cleanupClipMonitor(actionId);
695
877
  }
696
878
  const clipAction = this.clipActions.get(clipName);
697
879
  if (clipAction && clipAction !== action) {
880
+ const actionId = this.getActionId(clipAction);
698
881
  try {
699
882
  clipAction.stop();
700
883
  if (this.animationMixer) {
@@ -707,6 +890,7 @@ var BakedAnimationController = class {
707
890
  } catch {
708
891
  }
709
892
  this.clipActions.delete(clipName);
893
+ if (actionId) this.cleanupClipMonitor(actionId);
710
894
  }
711
895
  if (this.clipActions.get(clipName) === action) {
712
896
  this.clipActions.delete(clipName);
@@ -1268,6 +1452,7 @@ var BakedAnimationController = class {
1268
1452
  return null;
1269
1453
  }
1270
1454
  const clip = new AnimationClip(clipName, maxTime, tracks);
1455
+ this.setClipEventMetadata(clip, { keyframeTimes });
1271
1456
  console.log(`[Loom3] snippetToClip: Created clip "${clipName}" with ${tracks.length} tracks, duration ${maxTime.toFixed(2)}s`);
1272
1457
  return clip;
1273
1458
  }
@@ -1299,28 +1484,38 @@ var BakedAnimationController = class {
1299
1484
  this.animationClips.push(clip);
1300
1485
  }
1301
1486
  this.applyPlaybackState(action, playbackState);
1487
+ if (actionId) {
1488
+ this.cleanupClipMonitor(actionId);
1489
+ }
1302
1490
  let resolveFinished;
1303
1491
  const finishedPromise = new Promise((resolve) => {
1304
1492
  resolveFinished = resolve;
1305
1493
  });
1306
- const cleanup = () => {
1307
- try {
1308
- this.animationFinishedCallbacks.delete(clip.name);
1309
- } catch {
1310
- }
1311
- try {
1312
- action.paused = true;
1313
- } catch {
1314
- }
1494
+ const keyframeTimes = this.getClipEventMetadata(clip).keyframeTimes;
1495
+ const initialDirection = playbackState.reverse ? -1 : 1;
1496
+ const monitor = {
1497
+ action,
1498
+ actionId,
1499
+ clip,
1500
+ clipName: clip.name,
1501
+ duration: clip.duration,
1502
+ keyframeTimes,
1503
+ listeners: /* @__PURE__ */ new Set(),
1504
+ initialDirection,
1505
+ direction: initialDirection,
1506
+ iteration: 0,
1507
+ lastTime: Math.max(0, Math.min(clip.duration, action.time)),
1508
+ lastKeyframeIndex: this.getKeyframeIndex(keyframeTimes, action.time),
1509
+ loopMode: playbackState.loopMode,
1510
+ finishedPending: false,
1511
+ cleanedUp: false,
1512
+ resolveFinished
1315
1513
  };
1316
- this.animationFinishedCallbacks.set(clip.name, () => {
1317
- resolveFinished();
1318
- cleanup();
1319
- });
1320
- finishedPromise.catch(() => cleanup());
1514
+ this.clipMonitors.set(actionId, monitor);
1321
1515
  action.reset();
1322
1516
  action.time = startTime;
1323
1517
  action.play();
1518
+ this.resetClipMonitor(monitor, action.time);
1324
1519
  this.clipActions.set(clip.name, action);
1325
1520
  this.animationActions.set(clip.name, action);
1326
1521
  this.setPlaybackState(clip.name, playbackState);
@@ -1338,6 +1533,7 @@ var BakedAnimationController = class {
1338
1533
  })
1339
1534
  );
1340
1535
  action.play();
1536
+ this.resetClipMonitor(monitor, action.time);
1341
1537
  },
1342
1538
  stop: () => {
1343
1539
  action.stop();
@@ -1355,8 +1551,7 @@ var BakedAnimationController = class {
1355
1551
  this.animationActions.delete(clip.name);
1356
1552
  this.animationFinishedCallbacks.delete(clip.name);
1357
1553
  this.playbackState.delete(clip.name);
1358
- resolveFinished();
1359
- cleanup();
1554
+ this.cleanupClipMonitor(actionId);
1360
1555
  },
1361
1556
  pause: () => {
1362
1557
  action.paused = true;
@@ -1374,6 +1569,8 @@ var BakedAnimationController = class {
1374
1569
  const next = this.playbackState.get(clip.name) ?? playbackState;
1375
1570
  next.playbackRate = Number.isFinite(r) ? Math.max(0, Math.abs(r)) : 1;
1376
1571
  this.applyPlaybackState(action, next);
1572
+ monitor.direction = next.reverse ? -1 : 1;
1573
+ monitor.initialDirection = monitor.direction;
1377
1574
  this.setPlaybackState(clip.name, next);
1378
1575
  },
1379
1576
  setLoop: (mode, repeatCount) => {
@@ -1382,6 +1579,7 @@ var BakedAnimationController = class {
1382
1579
  next.loop = mode !== "once";
1383
1580
  next.repeatCount = repeatCount;
1384
1581
  this.applyPlaybackState(action, next);
1582
+ monitor.loopMode = mode;
1385
1583
  this.setPlaybackState(clip.name, next);
1386
1584
  },
1387
1585
  setTime: (t) => {
@@ -1391,9 +1589,16 @@ var BakedAnimationController = class {
1391
1589
  this.animationMixer?.update(0);
1392
1590
  } catch {
1393
1591
  }
1592
+ this.syncClipMonitorTime(monitor, clamped, true);
1394
1593
  },
1395
1594
  getTime: () => action.time,
1396
1595
  getDuration: () => clip.duration,
1596
+ subscribe: (listener) => {
1597
+ monitor.listeners.add(listener);
1598
+ return () => {
1599
+ monitor.listeners.delete(listener);
1600
+ };
1601
+ },
1397
1602
  finished: finishedPromise
1398
1603
  };
1399
1604
  this.clipHandles.set(clip.name, handle);
@@ -1417,6 +1622,7 @@ var BakedAnimationController = class {
1417
1622
  if (!this.animationMixer || !this.host.getModel()) return;
1418
1623
  for (const [clipName, action] of Array.from(this.clipActions.entries())) {
1419
1624
  if (clipName === name || clipName.startsWith(`${name}_`)) {
1625
+ const actionId = this.getActionId(action);
1420
1626
  try {
1421
1627
  action.stop();
1422
1628
  const clip = action.getClip();
@@ -1431,6 +1637,7 @@ var BakedAnimationController = class {
1431
1637
  this.clipHandles.delete(clipName);
1432
1638
  this.animationFinishedCallbacks.delete(clipName);
1433
1639
  this.playbackState.delete(clipName);
1640
+ if (actionId) this.cleanupClipMonitor(actionId);
1434
1641
  }
1435
1642
  }
1436
1643
  }
@@ -1454,6 +1661,8 @@ var BakedAnimationController = class {
1454
1661
  console.log("[Loom3] updateClipParams start", debugSnapshot());
1455
1662
  const apply = (action) => {
1456
1663
  if (!action) return;
1664
+ const actionId = this.getActionId(action);
1665
+ const monitor = actionId ? this.clipMonitors.get(actionId) : void 0;
1457
1666
  const clipName = action.getClip().name;
1458
1667
  const next = this.playbackState.get(clipName) ?? this.normalizePlaybackOptions(void 0, { loop: false, source: this.clipSources.get(clipName) ?? "clip" });
1459
1668
  try {
@@ -1472,6 +1681,10 @@ var BakedAnimationController = class {
1472
1681
  }
1473
1682
  const signedRate = next.reverse ? -next.playbackRate : next.playbackRate;
1474
1683
  action.setEffectiveTimeScale(signedRate);
1684
+ if (monitor) {
1685
+ monitor.direction = next.reverse ? -1 : 1;
1686
+ monitor.initialDirection = monitor.direction;
1687
+ }
1475
1688
  updated = true;
1476
1689
  }
1477
1690
  if (typeof params.loop === "boolean" || params.loopMode || params.repeatCount !== void 0) {
@@ -1479,6 +1692,7 @@ var BakedAnimationController = class {
1479
1692
  next.loop = next.loopMode !== "once";
1480
1693
  next.repeatCount = params.repeatCount;
1481
1694
  this.applyPlaybackState(action, next);
1695
+ if (monitor) monitor.loopMode = next.loopMode;
1482
1696
  updated = true;
1483
1697
  }
1484
1698
  this.setPlaybackState(clipName, next);
@@ -1558,6 +1772,14 @@ var BakedAnimationController = class {
1558
1772
  if (this.animationMixer && !this.mixerFinishedListenerAttached) {
1559
1773
  this.animationMixer.addEventListener("finished", (event) => {
1560
1774
  const action = event.action;
1775
+ const actionId = this.getActionId(action);
1776
+ if (actionId) {
1777
+ const monitor = this.clipMonitors.get(actionId);
1778
+ if (monitor) {
1779
+ monitor.finishedPending = true;
1780
+ return;
1781
+ }
1782
+ }
1561
1783
  const clip = action.getClip();
1562
1784
  const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
1563
1785
  if (bakedRuntime) {
@@ -4264,9 +4486,11 @@ function getPreset(presetType) {
4264
4486
  return CC4_PRESET;
4265
4487
  }
4266
4488
  }
4489
+ var resolvePreset = getPreset;
4267
4490
  function getPresetWithProfile(presetType, profile) {
4268
4491
  return extendPresetWithProfile(getPreset(presetType), profile);
4269
4492
  }
4493
+ var resolvePresetWithOverrides = getPresetWithProfile;
4270
4494
 
4271
4495
  // src/engines/three/Loom3.ts
4272
4496
  var deg2rad = (d) => d * Math.PI / 180;
@@ -7374,6 +7598,6 @@ async function analyzeModel(options) {
7374
7598
  };
7375
7599
  }
7376
7600
 
7377
- export { AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, FISH_AU_MAPPING_CONFIG, HairPhysics, Loom3, Loom3 as Loom3Three, Loom3 as LoomLargeThree, MORPH_TO_MESH, VISEME_JAW_AMOUNTS, VISEME_KEYS, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, suggestBestPreset, validateMappingConfig, validateMappings };
7601
+ export { AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, FISH_AU_MAPPING_CONFIG, HairPhysics, Loom3, Loom3 as Loom3Three, Loom3 as LoomLargeThree, MORPH_TO_MESH, VISEME_JAW_AMOUNTS, VISEME_KEYS, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, resolvePreset, resolvePresetWithOverrides, suggestBestPreset, validateMappingConfig, validateMappings };
7378
7602
  //# sourceMappingURL=index.js.map
7379
7603
  //# sourceMappingURL=index.js.map