Hello, I don't want to spam this thread but I had to fix two things...
1. Bug: duplicate morph names in the Add Targets list (v16)
storable.GetFloatParamNames() on
geometry returns both
morph: X and
morphOtherGender: X. Timeline 291 showed the raw names, so the two entries were distinguishable. The morph translation feature strips both prefixes in
MorphTranslation.GetDisplayNameRaw(), so every cross-gender morph now appears twice with exactly the same name. There is no way to tell which one is the same-gender morph, and picking the wrong one animates the other-gender morph.
The same duplicate names show up in the Dope Sheet labels, the target frame labels and the Float Curve Editor, since they all use
GetDisplayNameRaw().
File: src/AtomAnimations/Utils/MorphTranslation.cs, GetDisplayNameRaw(), line 41
Before:
C#:
if (rawName.StartsWith("morphOtherGender: "))
return GetDisplayName(rawName.Substring("morphOtherGender: ".Length));
After:
C#:
if (rawName.StartsWith("morphOtherGender: "))
return GetDisplayName(rawName.Substring("morphOtherGender: ".Length)) + " (other gender)";
This only changes the displayed label. The stored param name, the serialization and the target matching are unchanged.
2. Performance bug: “Load Preset” freezes VaM with large presets (v16)
With a preset of ~200 morphs + 17 controllers, Load Preset freezes VaM for a very long time.
Cause: LoadMorphPreset() adds targets one at a time through
clip.AddFloatParam() /
clip.AddController(). Each add calls
onTargetsListChanged.Invoke(), and every listener then does a full rebuild:
DopeSheet.OnTargetsListChanged() → UnbindClip() + BindClip(), destroying and recreating every row synchronously
AddRemoveTargetsScreen.OnTargetsListChanged() → RefreshStorableFloatsList(), which runs GetFloatParamNames() on thousands of morphs, sorts them and builds the translated display list
AtomAnimation.OnTargetsListChanged() → index.Rebuild()
So the total cost is O(n²): N targets × N rows rebuilt.
Fix: suppress the notification during the batch and fire it once per clip at the end.
1. src/AtomAnimations/Animations/AtomAnimationClip.cs
After line 55 (
onTargetsListChanged declaration), add:
C#:
public bool suppressTargetsListChanged;
At lines 894 and 948, replace:
C#:
onTargetsListChanged.Invoke();
with:
C#:
if (!suppressTargetsListChanged) onTargetsListChanged.Invoke();
2. src/UI/Screens/AddRemoveTargetsScreen.cs, LoadMorphPreset() (line 620)
Wrap the body of the existing
try block:
C#:
private void LoadMorphPreset()
{
try
{
var batchClips = currentLayer.ToList();
foreach (var bc in batchClips) bc.suppressTargetsListChanged = true;
try
{
animationEditContext.PushUndo();
// ... existing body unchanged ...
SuperController.LogMessage($"Timeline: Loaded preset '{name}': ...");
}
finally
{
foreach (var bc in batchClips)
{
bc.suppressTargetsListChanged = false;
bc.onTargetsListChanged.Invoke();
}
}
}
catch (Exception exc)
{
SuperController.LogError($"Timeline.{nameof(LoadMorphPreset)}: " + exc);
}
}
The result is the same, with one rebuild instead of one per target. The finally block makes sure the flag is always reset, even on an early return or an exception. The same pattern could be applied to any other batch add (Pose Index load, Clip Sequencer, etc.) if they go through the same per-target path.
Same issue in RemoveSelected() (AddRemoveTargetsScreen.cs), inherited from the original Timeline code: each
clip.Remove(target) fires
onTargetsListChanged, so removing N targets rebuilds the Dope Sheet N times. The fix uses the same
suppressTargetsListChanged flag: set it on
currentLayer clips before the loop, then reset it and invoke
onTargetsListChanged once per clip in a
finally block. Also move the
_addControllerListJSON.val /
_addStorableListJSON.val /
_addParamListJSON.val assignments out of the loop and apply them once for the last removed target, because the
Storable chooser callback calls
RefreshStorableFloatsList() each time.