Files
2026-07-13 13:13:17 +08:00

602 lines
23 KiB
C#

#nullable enable
using T3.Core.IO;
using T3.Core.Operator;
using T3.Core.Operator.Interfaces;
using T3.Core.Operator.Slots;
using T3.Core.Utils;
using T3.Editor.Gui.Interaction.Variations.Model;
using T3.Editor.Gui.Windows.Variations;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Commands;
using T3.Editor.UiModel.Commands.Graph;
using T3.Editor.UiModel.Commands.Variations;
using T3.Editor.UiModel.ProjectHandling;
namespace T3.Editor.Gui.Interaction.Variations;
/// <summary>
/// Applies actions on variations to the currently active pool.
/// </summary>
/// <remarks>
/// Variations are a sets of symbolChild.input-parameters combinations defined for an Symbol.
/// These input slots can also include the symbols out inputs which thus can be used for defining
/// and applying "presets" to instances of that symbol.
///
/// Most variations will modify(!) the parent symbol. This is great while working within a single symbol
/// and tweaking and blending parameters. However it's potentially unintended (or dangerous) if the
/// modified symbol has many instances. That's why applying symbol-variations is not allowed for Symbols
/// in the lib-namespace.
/// </remarks>
internal static class VariationHandling
{
public static SymbolVariationPool? ActivePoolForSnapshots { get; private set; }
public static Instance? ActiveInstanceForSnapshots { get; private set; }
public static SymbolVariationPool? ActivePoolForPresets { get; private set; }
public static Instance? ActiveInstanceForPresets { get; private set; }
/// <summary>
/// Update variation handling
/// </summary>
public static void Update()
{
// Sync with composition selected in UI
// var primaryGraphWindow = GraphWindow.Focused;
// if (primaryGraphWindow == null)
// return;
if (ProjectView.Focused == null)
return;
var nodeSelection = ProjectView.Focused.NodeSelection;
var singleSelectedInstance = nodeSelection.GetSelectedInstanceWithoutComposition();
if (singleSelectedInstance != null)
{
var selectedSymbolId = singleSelectedInstance.Symbol.Id;
ActivePoolForPresets = GetOrLoadVariations(selectedSymbolId);
if (singleSelectedInstance.Parent != null)
ActivePoolForSnapshots = GetOrLoadVariations(singleSelectedInstance.Parent.Symbol.Id);
ActiveInstanceForPresets = singleSelectedInstance;
ActiveInstanceForSnapshots = singleSelectedInstance.Parent;
}
else
{
ActivePoolForPresets = null;
var activeCompositionInstance = ProjectView.Focused.CompositionInstance;
ActiveInstanceForSnapshots = activeCompositionInstance;
// Prevent variations for library operators
if (activeCompositionInstance != null)
{
if (activeCompositionInstance.Symbol.Namespace.StartsWith("Lib."))
{
ActivePoolForSnapshots = null;
}
else
{
ActivePoolForSnapshots = GetOrLoadVariations(activeCompositionInstance.Symbol.Id);
}
}
if (!nodeSelection.IsAnythingSelected())
{
ActiveInstanceForPresets = ActiveInstanceForSnapshots;
}
}
BlendActions.SmoothVariationBlending.UpdateBlend();
ProcessSnapshotActivationRequests();
ProcessSnapshotBlendRequests();
// Frame-driven, window-independent: lets callers (e.g. the snapshot control view) render
// thumbnails without the Variations window being open.
VariationThumbnailRenderer.Update();
}
/// <summary>
/// Applies one-shot snapshot activations forwarded by [ActivateSnapshot] operators and clears them
/// (a pending entry means "activate now"). Activations only apply to the composition currently
/// active for snapshots.
/// </summary>
private static void ProcessSnapshotActivationRequests()
{
var pending = SnapShotBlendingData.PendingActivationsByComposition;
if (pending.Count == 0)
return;
foreach (var (compositionSymbolId, rawIndex) in pending)
{
if (ActiveInstanceForSnapshots != null && ActiveInstanceForSnapshots.Symbol.Id == compositionSymbolId)
SnapshotActions.ActivateSnapshotByModuloIndex(rawIndex);
}
pending.Clear();
}
/// <summary>
/// Applies any procedural snapshot-blend requests forwarded by [BlendSnapshots] operators. Each
/// operator re-arms its request every evaluation; we consume the flag here so an operator that
/// stops evaluating (deleted, disconnected, paused) releases its blend after one frame.
/// </summary>
private static void ProcessSnapshotBlendRequests()
{
foreach (var (compositionSymbolId, request) in SnapShotBlendingData.RequestsByComposition)
{
if (!request.Enabled)
{
ReleaseOpDrivenBlend(compositionSymbolId);
continue;
}
request.Enabled = false;
if (ActivePoolForSnapshots == null || ActiveInstanceForSnapshots == null
|| ActiveInstanceForSnapshots.Symbol.Id != compositionSymbolId)
{
request.ResolvedStatusLevel = IStatusProvider.StatusLevel.Notice;
request.ResolvedStatusMessage = "This composition is not currently active for snapshots.";
ReleaseOpDrivenBlend(compositionSymbolId);
continue;
}
ApplyOpDrivenBlend(ActivePoolForSnapshots, ActiveInstanceForSnapshots, request);
}
}
private static void ApplyOpDrivenBlend(SymbolVariationPool pool, Instance instance, SnapShotBlendingData.BlendRequest request)
{
_opOrderedSnapshots.Clear();
foreach (var v in pool.AllVariations)
{
if (v.IsSnapshot)
_opOrderedSnapshots.Add(v);
}
if (_opOrderedSnapshots.Count == 0)
{
request.ResolvedStatusLevel = IStatusProvider.StatusLevel.Warning;
request.ResolvedStatusMessage = "This composition has no snapshots.";
ReleaseOpDrivenBlend(instance.Symbol.Id);
return;
}
if (request.Mode == SnapShotBlendingData.IndexMode.SnapshotIndices)
VariationBaseCanvas.SortByActivationIndex(_opOrderedSnapshots);
_opBlendVariations.Clear();
_opBlendWeights.Clear();
var notFoundCount = 0;
for (var i = 0; i < request.Indices.Count; i++)
{
var weight = i < request.WeightFactors.Count ? request.WeightFactors[i] : 0f;
if (weight <= 0.0001f)
continue;
if (!TryResolveSnapshot(request.Mode, request.Indices[i], out var variation))
{
notFoundCount++;
continue;
}
_opBlendVariations.Add(variation);
_opBlendWeights.Add(weight);
}
if (_opBlendVariations.Count == 0)
{
if (notFoundCount > 0)
{
request.ResolvedStatusLevel = IStatusProvider.StatusLevel.Warning;
request.ResolvedStatusMessage = "None of the snapshot indices were found.";
}
else
{
request.ResolvedStatusLevel = IStatusProvider.StatusLevel.Notice;
request.ResolvedStatusMessage = "All weight factors are zero.";
}
ReleaseOpDrivenBlend(instance.Symbol.Id);
return;
}
// Normalize so the raw weighted-sum blend stays well-formed (weights must sum to 1).
var sum = 0f;
foreach (var w in _opBlendWeights)
sum += w;
if (sum > 0.0001f)
{
for (var i = 0; i < _opBlendWeights.Count; i++)
_opBlendWeights[i] /= sum;
}
// Re-applying an unchanged blend would needlessly rebuild the macro command every frame; the
// previous apply stays live until released, so a steady hold costs nothing.
var alreadyDriving = pool.IsBlendDrivenByOperator && _opDrivenCompositionId == instance.Symbol.Id;
if (!alreadyDriving || HasOpBlendChanged())
{
pool.ApplyOperatorDrivenBlend(instance, _opBlendVariations, _opBlendWeights);
pool.IsBlendDrivenByOperator = true;
_opDrivenCompositionId = instance.Symbol.Id;
CacheAppliedOpBlend();
}
if (notFoundCount > 0)
{
request.ResolvedStatusLevel = IStatusProvider.StatusLevel.Notice;
request.ResolvedStatusMessage = $"Blending {_opBlendVariations.Count} snapshot(s); {notFoundCount} index(es) not found.";
}
else
{
request.ResolvedStatusLevel = IStatusProvider.StatusLevel.Success;
request.ResolvedStatusMessage = $"Blending {_opBlendVariations.Count} snapshot(s).";
}
}
private static bool TryResolveSnapshot(SnapShotBlendingData.IndexMode mode, int index, out Variation variation)
{
if (mode == SnapShotBlendingData.IndexMode.ControllerIndices)
{
foreach (var v in _opOrderedSnapshots)
{
if (v.ActivationIndex == index)
{
variation = v;
return true;
}
}
variation = null!;
return false;
}
// Ordinal position in reading order (the list is pre-sorted by the caller).
if (index >= 0 && index < _opOrderedSnapshots.Count)
{
variation = _opOrderedSnapshots[index];
return true;
}
variation = null!;
return false;
}
private static void ReleaseOpDrivenBlend(Guid compositionSymbolId)
{
if (_opDrivenCompositionId != compositionSymbolId)
return;
var pool = GetOrLoadVariations(compositionSymbolId);
if (pool.IsBlendDrivenByOperator)
{
pool.StopHover();
pool.IsBlendDrivenByOperator = false;
// Collapse the weight vector back to the active variation so the picker stops showing the mix.
pool.ResetBlendWeights(pool.ActiveVariation?.Id ?? Guid.Empty);
}
_opDrivenCompositionId = Guid.Empty;
_lastAppliedVariationIds.Clear();
_lastAppliedWeights.Clear();
}
private static bool HasOpBlendChanged()
{
if (_lastAppliedVariationIds.Count != _opBlendVariations.Count)
return true;
for (var i = 0; i < _opBlendVariations.Count; i++)
{
if (_lastAppliedVariationIds[i] != _opBlendVariations[i].Id)
return true;
if (MathF.Abs(_lastAppliedWeights[i] - _opBlendWeights[i]) > 0.0005f)
return true;
}
return false;
}
private static void CacheAppliedOpBlend()
{
_lastAppliedVariationIds.Clear();
_lastAppliedWeights.Clear();
foreach (var v in _opBlendVariations)
_lastAppliedVariationIds.Add(v.Id);
foreach (var w in _opBlendWeights)
_lastAppliedWeights.Add(w);
}
public static SymbolVariationPool GetOrLoadVariations(Guid symbolId)
{
if (_variationPoolForOperators.TryGetValue(symbolId, out var variationForComposition))
{
return variationForComposition;
}
var newOpVariationPool = new SymbolVariationPool(symbolId);
_variationPoolForOperators[newOpVariationPool.SymbolId] = newOpVariationPool;
return newOpVariationPool;
}
private const int AutoIndex = -1;
/// <summary>
/// This tries to create at new variation and saves the variation file
/// </summary>
public static Variation? CreateOrUpdateSnapshotVariation(int activationIndex = AutoIndex)
{
// Only allow for snapshots.
if (ActivePoolForSnapshots == null || ActiveInstanceForSnapshots == null)
{
return null;
}
// Delete previous snapshot for that index.
if (activationIndex != AutoIndex && SymbolVariationPool.TryGetSnapshot(activationIndex, out var existingVariation))
{
ActivePoolForSnapshots.DeleteVariation(existingVariation);
}
_affectedInstances.Clear();
AddSnapshotEnabledChildrenToList(ActiveInstanceForSnapshots, _affectedInstances);
var activeBefore = ActivePoolForSnapshots.ActiveVariation;
if (!ActivePoolForSnapshots.TryCreateVariationForCompositionInstances(_affectedInstances, out var newVariation))
{
return null;
}
// The list now sorts by ActivationIndex, so placement is purely a matter of which index the new
// snapshot gets; its free canvas position is independent.
newVariation.PosOnCanvas = VariationBaseCanvas.FindFreePositionForNewThumbnail(ActivePoolForSnapshots.AllVariations);
if (activationIndex != AutoIndex)
{
// Explicit slot (e.g. a MIDI pad).
newVariation.ActivationIndex = activationIndex;
}
else if (activeBefore is { IsSnapshot: true })
{
// Insert right behind the active snapshot: the next free controller index after it.
newVariation.ActivationIndex = ActivePoolForSnapshots.GetNextFreeActivationIndexAfter(activeBefore.ActivationIndex, newVariation);
}
else
{
// No active snapshot (e.g. the first one): take the lowest free index so 0 (top-left) is used.
newVariation.ActivationIndex = ActivePoolForSnapshots.GetNextFreeActivationIndexAfter(-1, newVariation);
}
// Make the new snapshot the active one so every view (Variations window + control view)
// agrees. The values already equal the current state, so no apply command is needed.
ActivePoolForSnapshots.SetActiveVariationWithoutApply(newVariation);
ActivePoolForSnapshots.SaveVariationsToFile();
return newVariation;
}
public static void RemoveInstancesFromVariations(IEnumerable<Guid> symbolChildIds, IReadOnlyList<Variation> variations, MacroCommand? collectInto = null)
{
if (ActivePoolForSnapshots == null || ActiveInstanceForSnapshots == null)
{
return;
}
var command = new RemoveInstancesFromVariationsCommand(ActivePoolForSnapshots, symbolChildIds, variations);
if (collectInto != null)
collectInto.AddAndExecCommand(command);
else
UndoRedoStack.AddAndExecute(command);
}
/// <summary>
/// Toggles snapshot control for a single parameter as one undoable macro: updates the
/// child's enabled set (enabling the first / disabling the last parameter also flips the
/// per-op flag) and keeps all existing snapshots consistent — enabling captures the
/// parameter's current value, disabling removes its stored values.
/// </summary>
internal static void ToggleParameterSnapshotControl(SymbolUi compositionUi, SymbolUi.Child childUi, Symbol.Child.Input input, bool enable)
{
// ParameterCollections (group index above 1) keep their own semantics
if (childUi.SnapshotGroupIndex > 1)
return;
var inputId = input.InputDefinition.Id;
HashSet<Guid>? newEnabledIds;
int newGroupIndex;
if (enable)
{
if (childUi.IsInputEnabledForSnapshots(inputId))
return;
newGroupIndex = 1;
newEnabledIds = childUi.EnabledForSnapshots && childUi.SnapshotEnabledInputIds != null
? [..childUi.SnapshotEnabledInputIds]
: [];
newEnabledIds.Add(inputId);
}
else
{
if (!childUi.IsInputEnabledForSnapshots(inputId))
return;
// A null set means all parameters: materialize it before removing one
newEnabledIds = childUi.SnapshotEnabledInputIds != null
? [..childUi.SnapshotEnabledInputIds]
: CollectControlledInputIds(childUi);
newEnabledIds.Remove(inputId);
if (newEnabledIds.Count == 0)
{
newGroupIndex = 0;
newEnabledIds = null;
}
else
{
newGroupIndex = 1;
}
}
var macro = new MacroCommand("Toggle parameter snapshot control");
macro.AddAndExecCommand(new ChangeSnapshotEnabledInputsCommand(compositionUi.Symbol.Id, childUi, newGroupIndex, newEnabledIds));
var pool = GetOrLoadVariations(compositionUi.Symbol.Id);
foreach (var variation in pool.AllVariations)
{
if (variation.IsPreset)
continue;
if (enable)
{
// Default values are not stored — apply resets non-stored controlled params anyway
if (input.IsDefault)
continue;
var newSets = CloneParameterSetsWithValue(variation.ParameterSetsForChildIds, childUi.Id, inputId, input.Value);
macro.AddAndExecCommand(new UpdateVariationParametersCommand(pool, variation, newSets));
}
else
{
if (!variation.ParameterSetsForChildIds.TryGetValue(childUi.Id, out var storedSet))
continue;
var removeWholeChild = newGroupIndex == 0;
if (!removeWholeChild && !storedSet.ContainsKey(inputId))
continue;
var newSets = CloneParameterSetsWithoutValue(variation.ParameterSetsForChildIds, childUi.Id, inputId, removeWholeChild);
macro.AddAndExecCommand(new UpdateVariationParametersCommand(pool, variation, newSets));
}
}
UndoRedoStack.Add(macro);
}
/// <summary>
/// Writes <paramref name="value"/> as the stored value of one input (<paramref name="childId"/> /
/// <paramref name="inputId"/>) into each of the given snapshots, as a single undoable macro.
/// Backs the per-parameter "Apply to snapshot" / "Apply to all snapshots" actions in the
/// snapshot control view.
/// </summary>
internal static void ApplyParameterToVariations(SymbolVariationPool pool, IEnumerable<Variation> targets,
Guid childId, Guid inputId, InputValue value, string commandName)
{
var macro = new MacroCommand(commandName);
var any = false;
foreach (var variation in targets)
{
if (variation.IsPreset)
continue;
var newSets = CloneParameterSetsWithValue(variation.ParameterSetsForChildIds, childId, inputId, value);
macro.AddAndExecCommand(new UpdateVariationParametersCommand(pool, variation, newSets));
any = true;
}
if (any)
UndoRedoStack.Add(macro);
}
/// <summary>
/// All blendable, non-excluded input ids of the child's symbol — the parameters the
/// snapshot system can control. Used to materialize the legacy "all enabled" state.
/// </summary>
private static HashSet<Guid> CollectControlledInputIds(SymbolUi.Child childUi)
{
var result = new HashSet<Guid>();
var symbol = childUi.SymbolChild.Symbol;
var symbolUi = symbol.GetSymbolUi();
foreach (var inputDefinition in symbol.InputDefinitions)
{
if (!ValueUtils.BlendMethods.ContainsKey(inputDefinition.DefaultValue.ValueType))
continue;
if (symbolUi.InputUis.TryGetValue(inputDefinition.Id, out var inputUi) && inputUi.ExcludedFromPresets)
continue;
result.Add(inputDefinition.Id);
}
return result;
}
private static Dictionary<Guid, Dictionary<Guid, InputValue>> CloneParameterSetsWithValue(Dictionary<Guid, Dictionary<Guid, InputValue>> source,
Guid childId, Guid inputId, InputValue value)
{
var result = new Dictionary<Guid, Dictionary<Guid, InputValue>>(source);
result[childId] = result.TryGetValue(childId, out var childSet)
? new Dictionary<Guid, InputValue>(childSet)
: new Dictionary<Guid, InputValue>();
result[childId][inputId] = value.Clone();
return result;
}
private static Dictionary<Guid, Dictionary<Guid, InputValue>> CloneParameterSetsWithoutValue(Dictionary<Guid, Dictionary<Guid, InputValue>> source,
Guid childId, Guid inputId, bool removeWholeChild)
{
var result = new Dictionary<Guid, Dictionary<Guid, InputValue>>(source);
if (removeWholeChild)
{
result.Remove(childId);
return result;
}
if (result.TryGetValue(childId, out var childSet))
{
var newChildSet = new Dictionary<Guid, InputValue>(childSet);
newChildSet.Remove(inputId);
result[childId] = newChildSet;
}
return result;
}
internal static void AddSnapshotEnabledChildrenToList(Instance instance, List<Instance> list)
{
var compositionUi = instance.GetSymbolUi();
foreach (var childInstance in instance.Children.Values)
{
var symbolChildUi = compositionUi.ChildUis[childInstance.SymbolChildId]; // Debug.Assert(symbolChildUi != null);
if (!symbolChildUi.EnabledForSnapshots)
continue;
list.Add(childInstance);
}
}
// private static IEnumerable<Instance> GetSnapshotEnabledChildren(Instance instance)
// {
// var compositionUi = SymbolUiRegistry.Entries[instance.Symbol.Id];
// foreach (var childInstance in instance.Children)
// {
// var symbolChildUi = compositionUi.ChildUis.SingleOrDefault(cui => cui.Id == childInstance.SymbolChildId);
// Debug.Assert(symbolChildUi != null);
//
// if (symbolChildUi.SnapshotGroupIndex == 0)
// continue;
//
// yield return childInstance;
// }
// }
private static readonly Dictionary<Guid, SymbolVariationPool> _variationPoolForOperators = new();
private static readonly List<Instance> _affectedInstances = new(100);
// Reused buffers for procedural snapshot blending (per-frame, allocation-free).
private static readonly List<Variation> _opOrderedSnapshots = new();
private static readonly List<Variation> _opBlendVariations = new();
private static readonly List<float> _opBlendWeights = new();
private static readonly List<Guid> _lastAppliedVariationIds = new();
private static readonly List<float> _lastAppliedWeights = new();
private static Guid _opDrivenCompositionId = Guid.Empty;
}