@lovelace_lol/loom3 1.0.39 → 1.0.41

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
@@ -92,7 +92,7 @@ declare const CC4_PRESET: Profile;
92
92
  * - Scalars: extension wins when provided.
93
93
  * - Maps: shallow-merged by key, values cloned.
94
94
  * - Arrays: replaced when the extension provides them (except annotationRegions).
95
- * - annotationRegions: merged by region name, shallow field merge (extension wins).
95
+ * - annotationRegions: merged by region name, with nested camera/style fields preserved.
96
96
  */
97
97
  declare function extendPresetWithProfile(base: Profile, extension?: Partial<Profile>): Profile;
98
98
 
@@ -224,7 +224,7 @@ declare const FISH_AU_MAPPING_CONFIG: {
224
224
  * Loom3 - Preset Exports
225
225
  *
226
226
  * All AU presets are exported from here.
227
- * Frontend passes a presetType string and Loom3 looks up the preset internally.
227
+ * Frontend passes a presetType string and Loom3 looks up or extends the preset internally.
228
228
  */
229
229
 
230
230
  /**
@@ -315,6 +315,33 @@ interface CompositeRotationState {
315
315
  roll: number;
316
316
  }
317
317
  type RotationsState = Record<string, CompositeRotationState>;
318
+ type MorphTargetAttributeData = Float32Array | number[];
319
+ interface MorphTargetDelta {
320
+ /** Mesh name from the loaded Three.js scene. */
321
+ meshName: string;
322
+ /** Morph target name to add to morphTargetDictionary. */
323
+ name: string;
324
+ /** POSITION deltas, usually XYZ values relative to the base mesh. */
325
+ position: MorphTargetAttributeData;
326
+ /** Optional NORMAL deltas. */
327
+ normal?: MorphTargetAttributeData;
328
+ /** Optional TANGENT deltas. */
329
+ tangent?: MorphTargetAttributeData;
330
+ /** Whether deltas are relative to base attributes. Defaults to true. */
331
+ relative?: boolean;
332
+ }
333
+ interface AddMorphTargetOptions {
334
+ /** Replace an existing morph target with the same name. */
335
+ replace?: boolean;
336
+ /** Initialize the new or replaced influence value to zero. Defaults to true. */
337
+ resetInfluence?: boolean;
338
+ /**
339
+ * Replace and dispose the BufferGeometry instead of mutating it in place.
340
+ * Defaults to true because Three.js does not support mutating morph attributes
341
+ * after a geometry has rendered.
342
+ */
343
+ forceGeometryReplacement?: boolean;
344
+ }
318
345
  /** Source category for mixer-driven animations. */
319
346
  type AnimationSource = 'baked' | 'clip' | 'snippet';
320
347
  /** Shared blend-mode surface for downstream animation UIs. */
@@ -958,6 +985,25 @@ interface Animation {
958
985
  * @param meshNames - Optional specific meshes to target
959
986
  */
960
987
  transitionMorphInfluence(index: number, to: number, durationMs?: number, meshNames?: string[]): TransitionHandle;
988
+ /**
989
+ * Add or replace a runtime morph target on a mesh.
990
+ * Returns the morphTargetInfluences index assigned to the target.
991
+ */
992
+ addMorphTarget(target: MorphTargetDelta, options?: AddMorphTargetOptions): number;
993
+ /**
994
+ * Add multiple runtime morph targets and return their assigned indices keyed
995
+ * by "meshName:name".
996
+ */
997
+ addMorphTargets(targets: MorphTargetDelta[], options?: AddMorphTargetOptions): Record<string, number>;
998
+ /**
999
+ * Ensure a named morph influence slot exists on the mesh.
1000
+ * If the target is missing, a zero-delta morph target is created.
1001
+ */
1002
+ ensureMorphInfluence(meshName: string, morphName: string): number;
1003
+ /**
1004
+ * Rebuild runtime morph caches after external geometry or dictionary changes.
1005
+ */
1006
+ refreshMorphTargets(meshNames?: string[]): void;
961
1007
  /**
962
1008
  * Set viseme value immediately (for lip-sync)
963
1009
  * @param visemeIndex - Viseme index 0-14
@@ -1461,6 +1507,10 @@ declare class Loom3 implements LoomLarge {
1461
1507
  * @param balance - Optional L/R balance for bilateral morphs
1462
1508
  */
1463
1509
  transitionContinuum(negAU: number, posAU: number, continuumValue: number, durationMs?: number, balance?: number): TransitionHandle;
1510
+ addMorphTarget(target: MorphTargetDelta, options?: AddMorphTargetOptions): number;
1511
+ addMorphTargets(targets: MorphTargetDelta[], options?: AddMorphTargetOptions): Record<string, number>;
1512
+ ensureMorphInfluence(meshName: string, morphName: string): number;
1513
+ refreshMorphTargets(_meshNames?: string[]): void;
1464
1514
  /**
1465
1515
  * Set a morph target value.
1466
1516
  *
@@ -1607,6 +1657,12 @@ declare class Loom3 implements LoomLarge {
1607
1657
  private applyMorphTargets;
1608
1658
  private getMorphValue;
1609
1659
  private getMorphValueByIndex;
1660
+ private applyMorphTargetDelta;
1661
+ private requireNamedMesh;
1662
+ private getMeshMorphDictionary;
1663
+ private setMorphAttributeAtIndex;
1664
+ private setZeroMorphAttributeAtIndex;
1665
+ private addRuntimeMorphMesh;
1610
1666
  private getMorphKeyCacheKey;
1611
1667
  private getMorphIndexCacheKey;
1612
1668
  private syncVisemeRuntimeState;
@@ -2002,10 +2058,9 @@ declare function applyCharacterProfileToPreset(config: CharacterConfig): Profile
2002
2058
  *
2003
2059
  * Precedence:
2004
2060
  * 1. preset defaults
2005
- * 2. saved `config.regions` overrides, or fallbacks when `annotationRegions`
2006
- * is present
2061
+ * 2. canonical flattened `annotationRegions` / top-level profile overrides
2007
2062
  * 3. legacy nested `config.profile` overrides (compatibility only)
2008
- * 4. flattened top-level profile overrides
2063
+ * 4. legacy `config.regions` fallback only when canonical annotation overrides are absent
2009
2064
  */
2010
2065
  declare function extendCharacterConfigWithPreset(config: CharacterConfig): CharacterConfig;
2011
2066
 
@@ -2462,4 +2517,4 @@ declare function detectFacingDirection(model: THREE.Object3D, eyeBoneNames?: {
2462
2517
  right: string[];
2463
2518
  }): 'forward' | 'backward' | 'unknown';
2464
2519
 
2465
- 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 CameraRelativeGazeOffset, type CameraRelativeGazeOptions, 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, computeCameraRelativeGazeOffset, 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 };
2520
+ export { type AUInfo, type AUSelector, AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, type AddMorphTargetOptions, 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 CameraRelativeGazeOffset, type CameraRelativeGazeOptions, 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 MorphTargetAttributeData, type MorphTargetDelta, 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, computeCameraRelativeGazeOffset, 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
@@ -92,7 +92,7 @@ declare const CC4_PRESET: Profile;
92
92
  * - Scalars: extension wins when provided.
93
93
  * - Maps: shallow-merged by key, values cloned.
94
94
  * - Arrays: replaced when the extension provides them (except annotationRegions).
95
- * - annotationRegions: merged by region name, shallow field merge (extension wins).
95
+ * - annotationRegions: merged by region name, with nested camera/style fields preserved.
96
96
  */
97
97
  declare function extendPresetWithProfile(base: Profile, extension?: Partial<Profile>): Profile;
98
98
 
@@ -224,7 +224,7 @@ declare const FISH_AU_MAPPING_CONFIG: {
224
224
  * Loom3 - Preset Exports
225
225
  *
226
226
  * All AU presets are exported from here.
227
- * Frontend passes a presetType string and Loom3 looks up the preset internally.
227
+ * Frontend passes a presetType string and Loom3 looks up or extends the preset internally.
228
228
  */
229
229
 
230
230
  /**
@@ -315,6 +315,33 @@ interface CompositeRotationState {
315
315
  roll: number;
316
316
  }
317
317
  type RotationsState = Record<string, CompositeRotationState>;
318
+ type MorphTargetAttributeData = Float32Array | number[];
319
+ interface MorphTargetDelta {
320
+ /** Mesh name from the loaded Three.js scene. */
321
+ meshName: string;
322
+ /** Morph target name to add to morphTargetDictionary. */
323
+ name: string;
324
+ /** POSITION deltas, usually XYZ values relative to the base mesh. */
325
+ position: MorphTargetAttributeData;
326
+ /** Optional NORMAL deltas. */
327
+ normal?: MorphTargetAttributeData;
328
+ /** Optional TANGENT deltas. */
329
+ tangent?: MorphTargetAttributeData;
330
+ /** Whether deltas are relative to base attributes. Defaults to true. */
331
+ relative?: boolean;
332
+ }
333
+ interface AddMorphTargetOptions {
334
+ /** Replace an existing morph target with the same name. */
335
+ replace?: boolean;
336
+ /** Initialize the new or replaced influence value to zero. Defaults to true. */
337
+ resetInfluence?: boolean;
338
+ /**
339
+ * Replace and dispose the BufferGeometry instead of mutating it in place.
340
+ * Defaults to true because Three.js does not support mutating morph attributes
341
+ * after a geometry has rendered.
342
+ */
343
+ forceGeometryReplacement?: boolean;
344
+ }
318
345
  /** Source category for mixer-driven animations. */
319
346
  type AnimationSource = 'baked' | 'clip' | 'snippet';
320
347
  /** Shared blend-mode surface for downstream animation UIs. */
@@ -958,6 +985,25 @@ interface Animation {
958
985
  * @param meshNames - Optional specific meshes to target
959
986
  */
960
987
  transitionMorphInfluence(index: number, to: number, durationMs?: number, meshNames?: string[]): TransitionHandle;
988
+ /**
989
+ * Add or replace a runtime morph target on a mesh.
990
+ * Returns the morphTargetInfluences index assigned to the target.
991
+ */
992
+ addMorphTarget(target: MorphTargetDelta, options?: AddMorphTargetOptions): number;
993
+ /**
994
+ * Add multiple runtime morph targets and return their assigned indices keyed
995
+ * by "meshName:name".
996
+ */
997
+ addMorphTargets(targets: MorphTargetDelta[], options?: AddMorphTargetOptions): Record<string, number>;
998
+ /**
999
+ * Ensure a named morph influence slot exists on the mesh.
1000
+ * If the target is missing, a zero-delta morph target is created.
1001
+ */
1002
+ ensureMorphInfluence(meshName: string, morphName: string): number;
1003
+ /**
1004
+ * Rebuild runtime morph caches after external geometry or dictionary changes.
1005
+ */
1006
+ refreshMorphTargets(meshNames?: string[]): void;
961
1007
  /**
962
1008
  * Set viseme value immediately (for lip-sync)
963
1009
  * @param visemeIndex - Viseme index 0-14
@@ -1461,6 +1507,10 @@ declare class Loom3 implements LoomLarge {
1461
1507
  * @param balance - Optional L/R balance for bilateral morphs
1462
1508
  */
1463
1509
  transitionContinuum(negAU: number, posAU: number, continuumValue: number, durationMs?: number, balance?: number): TransitionHandle;
1510
+ addMorphTarget(target: MorphTargetDelta, options?: AddMorphTargetOptions): number;
1511
+ addMorphTargets(targets: MorphTargetDelta[], options?: AddMorphTargetOptions): Record<string, number>;
1512
+ ensureMorphInfluence(meshName: string, morphName: string): number;
1513
+ refreshMorphTargets(_meshNames?: string[]): void;
1464
1514
  /**
1465
1515
  * Set a morph target value.
1466
1516
  *
@@ -1607,6 +1657,12 @@ declare class Loom3 implements LoomLarge {
1607
1657
  private applyMorphTargets;
1608
1658
  private getMorphValue;
1609
1659
  private getMorphValueByIndex;
1660
+ private applyMorphTargetDelta;
1661
+ private requireNamedMesh;
1662
+ private getMeshMorphDictionary;
1663
+ private setMorphAttributeAtIndex;
1664
+ private setZeroMorphAttributeAtIndex;
1665
+ private addRuntimeMorphMesh;
1610
1666
  private getMorphKeyCacheKey;
1611
1667
  private getMorphIndexCacheKey;
1612
1668
  private syncVisemeRuntimeState;
@@ -2002,10 +2058,9 @@ declare function applyCharacterProfileToPreset(config: CharacterConfig): Profile
2002
2058
  *
2003
2059
  * Precedence:
2004
2060
  * 1. preset defaults
2005
- * 2. saved `config.regions` overrides, or fallbacks when `annotationRegions`
2006
- * is present
2061
+ * 2. canonical flattened `annotationRegions` / top-level profile overrides
2007
2062
  * 3. legacy nested `config.profile` overrides (compatibility only)
2008
- * 4. flattened top-level profile overrides
2063
+ * 4. legacy `config.regions` fallback only when canonical annotation overrides are absent
2009
2064
  */
2010
2065
  declare function extendCharacterConfigWithPreset(config: CharacterConfig): CharacterConfig;
2011
2066
 
@@ -2462,4 +2517,4 @@ declare function detectFacingDirection(model: THREE.Object3D, eyeBoneNames?: {
2462
2517
  right: string[];
2463
2518
  }): 'forward' | 'backward' | 'unknown';
2464
2519
 
2465
- 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 CameraRelativeGazeOffset, type CameraRelativeGazeOptions, 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, computeCameraRelativeGazeOffset, 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 };
2520
+ export { type AUInfo, type AUSelector, AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, type AddMorphTargetOptions, 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 CameraRelativeGazeOffset, type CameraRelativeGazeOptions, 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 MorphTargetAttributeData, type MorphTargetDelta, 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, computeCameraRelativeGazeOffset, 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
@@ -1,5 +1,5 @@
1
1
  import * as THREE2 from 'three';
2
- import { Vector3, Clock, Box3, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh, PropertyBinding } from 'three';
2
+ import { Vector3, Clock, Box3, BufferAttribute, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh, PropertyBinding } from 'three';
3
3
 
4
4
  var __defProp = Object.defineProperty;
5
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -3552,7 +3552,7 @@ var mergeAnnotationRegion = (base, override) => {
3552
3552
  merged.meshes = override.meshes ? [...override.meshes] : base.meshes ? [...base.meshes] : void 0;
3553
3553
  merged.objects = override.objects ? [...override.objects] : base.objects ? [...base.objects] : void 0;
3554
3554
  merged.children = override.children ? [...override.children] : base.children ? [...base.children] : void 0;
3555
- merged.cameraOffset = override.cameraOffset ? { ...override.cameraOffset } : base.cameraOffset ? { ...base.cameraOffset } : void 0;
3555
+ merged.cameraOffset = override.cameraOffset ? { ...base.cameraOffset, ...override.cameraOffset } : base.cameraOffset ? { ...base.cameraOffset } : void 0;
3556
3556
  merged.style = override.style ? {
3557
3557
  ...base.style,
3558
3558
  ...override.style,
@@ -4617,11 +4617,7 @@ var _Loom3 = class _Loom3 {
4617
4617
  this.morphIndexCache.clear();
4618
4618
  model.traverse((obj) => {
4619
4619
  if (obj.isMesh && obj.name) {
4620
- const infl = obj.morphTargetInfluences;
4621
- const dict = obj.morphTargetDictionary;
4622
- if (Array.isArray(infl) && infl.length > 0 || dict && Object.keys(dict).length > 0) {
4623
- this.meshByName.set(obj.name, obj);
4624
- }
4620
+ this.meshByName.set(obj.name, obj);
4625
4621
  }
4626
4622
  });
4627
4623
  this.bones = this.resolveBones(model);
@@ -4989,6 +4985,58 @@ var _Loom3 = class _Loom3 {
4989
4985
  const currentContinuum = currentPos - currentNeg;
4990
4986
  return this.animation.addTransition(driverKey, currentContinuum, target, durationMs, (value) => this.setContinuum(negAU, posAU, value, balance));
4991
4987
  }
4988
+ // ============================================================================
4989
+ // MORPH CONTROL
4990
+ // ============================================================================
4991
+ addMorphTarget(target, options = {}) {
4992
+ const staleMorphTargets = this.collectResolvedExpressionMorphTargets();
4993
+ const index = this.applyMorphTargetDelta(target, options);
4994
+ this.refreshMorphTargets([target.meshName]);
4995
+ this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
4996
+ return index;
4997
+ }
4998
+ addMorphTargets(targets, options = {}) {
4999
+ const staleMorphTargets = this.collectResolvedExpressionMorphTargets();
5000
+ const result = {};
5001
+ for (const target of targets) {
5002
+ const index = this.applyMorphTargetDelta(target, options);
5003
+ result[`${target.meshName}:${target.name}`] = index;
5004
+ }
5005
+ this.refreshMorphTargets(Array.from(new Set(targets.map((target) => target.meshName))));
5006
+ this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
5007
+ return result;
5008
+ }
5009
+ ensureMorphInfluence(meshName, morphName) {
5010
+ const mesh = this.requireNamedMesh(meshName);
5011
+ const dict = this.getMeshMorphDictionary(mesh);
5012
+ const existing = dict[morphName];
5013
+ if (existing !== void 0) return existing;
5014
+ const position = mesh.geometry.getAttribute("position");
5015
+ if (!position) {
5016
+ throw new Error(`Cannot create morph target "${morphName}" on mesh "${meshName}": geometry has no position attribute.`);
5017
+ }
5018
+ return this.addMorphTarget({
5019
+ meshName,
5020
+ name: morphName,
5021
+ position: new Float32Array(position.count * position.itemSize),
5022
+ relative: true
5023
+ });
5024
+ }
5025
+ refreshMorphTargets(_meshNames) {
5026
+ this.morphKeyCache.clear();
5027
+ this.morphIndexCache.clear();
5028
+ if (this.model) {
5029
+ this.meshByName.clear();
5030
+ this.model.traverse((obj) => {
5031
+ if (obj.isMesh && obj.name) {
5032
+ this.meshByName.set(obj.name, obj);
5033
+ }
5034
+ });
5035
+ this.meshes = collectMorphMeshes(this.model);
5036
+ }
5037
+ this.rebuildMorphTargetsCache();
5038
+ this.hairPhysics.refreshMeshSelection();
5039
+ }
4992
5040
  setMorph(key, v, meshNamesOrTargets) {
4993
5041
  const val = clamp012(v);
4994
5042
  if (Array.isArray(meshNamesOrTargets) && meshNamesOrTargets.length > 0 && typeof meshNamesOrTargets[0] === "object" && "infl" in meshNamesOrTargets[0]) {
@@ -5607,6 +5655,148 @@ var _Loom3 = class _Loom3 {
5607
5655
  }
5608
5656
  return 0;
5609
5657
  }
5658
+ applyMorphTargetDelta(target, options) {
5659
+ const mesh = this.requireNamedMesh(target.meshName);
5660
+ const sourceGeometry = mesh.geometry;
5661
+ const position = sourceGeometry.getAttribute("position");
5662
+ if (!position) {
5663
+ throw new Error(`Cannot add morph target "${target.name}" to mesh "${target.meshName}": geometry has no position attribute.`);
5664
+ }
5665
+ if (!target.name || !target.name.trim()) {
5666
+ throw new Error(`Cannot add morph target to mesh "${target.meshName}": target name is required.`);
5667
+ }
5668
+ const replace = options.replace === true;
5669
+ const resetInfluence = options.resetInfluence !== false;
5670
+ const forceGeometryReplacement = options.forceGeometryReplacement !== false;
5671
+ const previousInfluences = mesh.morphTargetInfluences ? [...mesh.morphTargetInfluences] : [];
5672
+ const previousDictionary = this.getMeshMorphDictionary(mesh);
5673
+ const existingIndex = previousDictionary[target.name];
5674
+ if (existingIndex !== void 0 && !replace) {
5675
+ throw new Error(`Morph target "${target.name}" already exists on mesh "${target.meshName}". Pass replace: true to overwrite it.`);
5676
+ }
5677
+ const geometry = forceGeometryReplacement ? sourceGeometry.clone() : sourceGeometry;
5678
+ const dictionary = { ...previousDictionary };
5679
+ const usedIndices = Object.values(dictionary).filter(Number.isInteger);
5680
+ const existingAttributeTargetCount = Math.max(
5681
+ 0,
5682
+ ...Object.values(geometry.morphAttributes).map((attributes) => attributes?.length ?? 0)
5683
+ );
5684
+ const nextIndex = Math.max(existingAttributeTargetCount, usedIndices.length ? Math.max(...usedIndices) + 1 : 0);
5685
+ const index = existingIndex ?? nextIndex;
5686
+ dictionary[target.name] = index;
5687
+ this.setMorphAttributeAtIndex(geometry, "position", target.position, position.itemSize, position.count, index, target.name);
5688
+ const normal = geometry.getAttribute("normal");
5689
+ if (target.normal) {
5690
+ this.setMorphAttributeAtIndex(geometry, "normal", target.normal, normal?.itemSize ?? 3, position.count, index, target.name);
5691
+ } else {
5692
+ this.setZeroMorphAttributeAtIndex(geometry, "normal", normal?.itemSize ?? 3, position.count, index, target.name);
5693
+ }
5694
+ const tangent = geometry.getAttribute("tangent");
5695
+ if (target.tangent) {
5696
+ this.setMorphAttributeAtIndex(geometry, "tangent", target.tangent, tangent?.itemSize ?? 4, position.count, index, target.name);
5697
+ } else {
5698
+ this.setZeroMorphAttributeAtIndex(geometry, "tangent", tangent?.itemSize ?? 4, position.count, index, target.name);
5699
+ }
5700
+ const color = geometry.getAttribute("color");
5701
+ const existingColorMorph = geometry.morphAttributes.color?.find(Boolean);
5702
+ this.setZeroMorphAttributeAtIndex(
5703
+ geometry,
5704
+ "color",
5705
+ color?.itemSize ?? existingColorMorph?.itemSize ?? 3,
5706
+ position.count,
5707
+ index,
5708
+ target.name
5709
+ );
5710
+ geometry.morphTargetsRelative = target.relative !== false;
5711
+ geometry.morphTargetDictionary = dictionary;
5712
+ if (forceGeometryReplacement) {
5713
+ mesh.geometry = geometry;
5714
+ sourceGeometry.dispose();
5715
+ }
5716
+ const influenceLength = Math.max(previousInfluences.length, index + 1);
5717
+ const influences = previousInfluences.slice(0, influenceLength);
5718
+ while (influences.length < influenceLength) {
5719
+ influences.push(0);
5720
+ }
5721
+ if (resetInfluence) {
5722
+ influences[index] = 0;
5723
+ }
5724
+ mesh.morphTargetDictionary = dictionary;
5725
+ mesh.morphTargetInfluences = influences;
5726
+ this.addRuntimeMorphMesh(mesh);
5727
+ if (!this.config.morphToMesh?.face?.length) {
5728
+ this.config.morphToMesh = {
5729
+ ...this.config.morphToMesh,
5730
+ face: [mesh.name]
5731
+ };
5732
+ }
5733
+ return index;
5734
+ }
5735
+ requireNamedMesh(meshName) {
5736
+ const mesh = this.meshByName.get(meshName);
5737
+ if (mesh) return mesh;
5738
+ if (this.model) {
5739
+ let found = null;
5740
+ this.model.traverse((obj) => {
5741
+ if (!found && obj.isMesh && obj.name === meshName) {
5742
+ found = obj;
5743
+ }
5744
+ });
5745
+ if (found) {
5746
+ this.meshByName.set(meshName, found);
5747
+ return found;
5748
+ }
5749
+ }
5750
+ throw new Error(`Mesh "${meshName}" was not found in the current model.`);
5751
+ }
5752
+ getMeshMorphDictionary(mesh) {
5753
+ const meshDictionary = mesh.morphTargetDictionary;
5754
+ const geometryDictionary = mesh.geometry.morphTargetDictionary;
5755
+ const dictionary = meshDictionary || geometryDictionary || {};
5756
+ mesh.morphTargetDictionary = dictionary;
5757
+ mesh.geometry.morphTargetDictionary = dictionary;
5758
+ return dictionary;
5759
+ }
5760
+ setMorphAttributeAtIndex(geometry, semantic, data, itemSize, vertexCount, index, name) {
5761
+ const expectedLength = vertexCount * itemSize;
5762
+ if (data.length !== expectedLength) {
5763
+ throw new Error(
5764
+ `Morph target "${name}" ${semantic} data has ${data.length} values; expected ${expectedLength} (${vertexCount} vertices * itemSize ${itemSize}).`
5765
+ );
5766
+ }
5767
+ const attributes = geometry.morphAttributes[semantic] ? [...geometry.morphAttributes[semantic]] : [];
5768
+ while (attributes.length < index) {
5769
+ const empty = new BufferAttribute(new Float32Array(expectedLength), itemSize);
5770
+ empty.name = `morph_${attributes.length}`;
5771
+ attributes.push(empty);
5772
+ }
5773
+ const values = data instanceof Float32Array ? new Float32Array(data) : Float32Array.from(data);
5774
+ const attribute = new BufferAttribute(values, itemSize);
5775
+ attribute.name = name;
5776
+ attributes[index] = attribute;
5777
+ geometry.morphAttributes[semantic] = attributes;
5778
+ }
5779
+ setZeroMorphAttributeAtIndex(geometry, semantic, itemSize, vertexCount, index, name) {
5780
+ if (!geometry.morphAttributes[semantic]?.length) return;
5781
+ const expectedLength = vertexCount * itemSize;
5782
+ const attributes = [...geometry.morphAttributes[semantic]];
5783
+ while (attributes.length < index) {
5784
+ const empty2 = new BufferAttribute(new Float32Array(expectedLength), itemSize);
5785
+ empty2.name = `morph_${attributes.length}`;
5786
+ attributes.push(empty2);
5787
+ }
5788
+ const empty = new BufferAttribute(new Float32Array(expectedLength), itemSize);
5789
+ empty.name = name;
5790
+ attributes[index] = empty;
5791
+ geometry.morphAttributes[semantic] = attributes;
5792
+ }
5793
+ addRuntimeMorphMesh(mesh) {
5794
+ const key = mesh.name || mesh.uuid;
5795
+ const exists = this.meshes.some((candidate) => (candidate.name || candidate.uuid) === key);
5796
+ if (!exists) {
5797
+ this.meshes.push(mesh);
5798
+ }
5799
+ }
5610
5800
  getMorphKeyCacheKey(key, meshNames) {
5611
5801
  return meshNames?.length ? `key:${key}@${meshNames.join(",")}` : `key:${key}`;
5612
5802
  }
@@ -6201,22 +6391,32 @@ function mergeRegionsByName(base, override) {
6201
6391
  }
6202
6392
  return Array.from(merged.values());
6203
6393
  }
6394
+ function getAnnotationRegions(value) {
6395
+ return Array.isArray(value) ? value : void 0;
6396
+ }
6397
+ function getLegacyNestedOverrides(config) {
6398
+ return isPlainObject2(config.profile) ? config.profile : {};
6399
+ }
6400
+ function getLegacyRuntimeRegions(config) {
6401
+ return Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
6402
+ }
6403
+ function getCanonicalAnnotationOverrides(config) {
6404
+ return mergeRegionsByName(
6405
+ getAnnotationRegions(getLegacyNestedOverrides(config).annotationRegions),
6406
+ getAnnotationRegions(config.annotationRegions)
6407
+ );
6408
+ }
6204
6409
  function extractProfileOverrides(config) {
6205
6410
  const topLevelConfig = config;
6206
- const legacyNestedOverrides = isPlainObject2(config.profile) ? config.profile : {};
6411
+ const legacyNestedOverrides = getLegacyNestedOverrides(config);
6412
+ const canonicalAnnotationOverrides = getCanonicalAnnotationOverrides(config);
6413
+ const legacyRuntimeRegions = getLegacyRuntimeRegions(config);
6414
+ const annotationOverrides = canonicalAnnotationOverrides ?? (legacyRuntimeRegions ? legacyRuntimeRegions.map((region) => cloneRegion(region)) : void 0);
6207
6415
  const overrides = {};
6208
6416
  for (const key of PROFILE_OVERRIDE_KEYS) {
6209
6417
  if (key === "annotationRegions") {
6210
- const topLevelAnnotationRegions = Array.isArray(topLevelConfig.annotationRegions) ? topLevelConfig.annotationRegions : void 0;
6211
- const legacyAnnotationRegions = Array.isArray(legacyNestedOverrides.annotationRegions) ? legacyNestedOverrides.annotationRegions : void 0;
6212
- const legacyRegionFallback = Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
6213
- const legacyProfileRegions = mergeRegionsByName(legacyRegionFallback, legacyAnnotationRegions);
6214
- const regions = mergeRegionsByName(
6215
- legacyProfileRegions,
6216
- topLevelAnnotationRegions
6217
- );
6218
- if (regions) {
6219
- overrides.annotationRegions = regions.map((region) => cloneRegion(region));
6418
+ if (annotationOverrides) {
6419
+ overrides.annotationRegions = annotationOverrides.map((region) => cloneRegion(region));
6220
6420
  }
6221
6421
  continue;
6222
6422
  }
@@ -6229,13 +6429,6 @@ function extractProfileOverrides(config) {
6229
6429
  }
6230
6430
  return overrides;
6231
6431
  }
6232
- function hasCanonicalAnnotationRegionOverrides(config) {
6233
- const topLevelConfig = config;
6234
- if (Array.isArray(topLevelConfig.annotationRegions)) {
6235
- return true;
6236
- }
6237
- return isPlainObject2(config.profile) && Array.isArray(config.profile.annotationRegions);
6238
- }
6239
6432
  function applyCharacterProfileToPreset(config) {
6240
6433
  const presetType = config.auPresetType;
6241
6434
  if (!presetType) {
@@ -6267,24 +6460,34 @@ function extendCharacterConfigWithPreset(config) {
6267
6460
  if (!presetType || presetType === "custom") {
6268
6461
  return config;
6269
6462
  }
6463
+ const canonicalAnnotationOverrides = getCanonicalAnnotationOverrides(config);
6464
+ const legacyRuntimeRegions = getLegacyRuntimeRegions(config);
6270
6465
  const profileOverrides = extractProfileOverrides(config);
6271
6466
  const extendedPresetProfile = applyCharacterProfileToPreset(config);
6272
6467
  if (!extendedPresetProfile) {
6273
6468
  return config;
6274
6469
  }
6275
- const presetRegions = extendedPresetProfile.annotationRegions;
6276
- const mergedRegions = hasCanonicalAnnotationRegionOverrides(config) ? mergeRegionsByName(config.regions, presetRegions) : mergeRegionsByName(presetRegions, config.regions);
6277
- const normalizedRegions = normalizeRegionTree(
6278
- mergedRegions,
6470
+ const presetRegionNames = new Set(
6471
+ (getPreset(presetType).annotationRegions ?? []).map((region) => region.name)
6472
+ );
6473
+ const extendedAnnotationRegions = normalizeRegionTree(
6474
+ extendedPresetProfile.annotationRegions,
6475
+ profileOverrides.disabledRegions
6476
+ );
6477
+ const extendedRegionNames = new Set((extendedAnnotationRegions ?? []).map((region) => region.name));
6478
+ const legacyExtraRegions = canonicalAnnotationOverrides && legacyRuntimeRegions ? legacyRuntimeRegions.filter((region) => !presetRegionNames.has(region.name) && !extendedRegionNames.has(region.name)).map((region) => cloneRegion(region)) : void 0;
6479
+ const mergedRegions = normalizeRegionTree(
6480
+ mergeRegionsByName(extendedAnnotationRegions, legacyExtraRegions),
6279
6481
  profileOverrides.disabledRegions
6280
6482
  );
6281
6483
  const extendedRegions = orderExtendedRegions(
6282
- normalizedRegions,
6283
- [config.regions, profileOverrides.annotationRegions, presetRegions]
6484
+ mergedRegions,
6485
+ canonicalAnnotationOverrides ? [extendedAnnotationRegions, legacyExtraRegions] : [legacyRuntimeRegions, extendedAnnotationRegions]
6284
6486
  );
6285
6487
  return {
6286
6488
  ...config,
6287
6489
  ...extendedPresetProfile,
6490
+ annotationRegions: extendedRegions ?? extendedAnnotationRegions,
6288
6491
  regions: extendedRegions ?? config.regions
6289
6492
  };
6290
6493
  }
@@ -7444,7 +7647,7 @@ function extractMorphs(meshes) {
7444
7647
  for (const mesh of meshes) {
7445
7648
  const geo = mesh.geometry;
7446
7649
  if (!geo.morphAttributes) continue;
7447
- const dict = geo.morphTargetDictionary;
7650
+ const dict = mesh.morphTargetDictionary || geo.morphTargetDictionary;
7448
7651
  if (dict) {
7449
7652
  for (const [name, index] of Object.entries(dict)) {
7450
7653
  morphs.push({