#nullable enable
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using T3.Core.Model;
using T3.Core.Operator;
using T3.Core.Operator.Slots;
using T3.Core.Settings;
using T3.Core.Utils;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Commands;
using T3.Editor.UiModel.Commands.Graph;
using T3.Editor.UiModel.Commands.Variations;
using T3.Serialization;
namespace T3.Editor.Gui.Interaction.Variations.Model;
///
/// Collects all presets and variations for a symbol.
///
internal sealed class SymbolVariationPool
{
public readonly Guid SymbolId;
public readonly IReadOnlyList AllVariations;
public readonly IReadOnlyList UserVariations;
public readonly IReadOnlyList Defaults;
private readonly List _userVariations;
private readonly List _defaults;
private readonly List _allVariations;
private Variation? _activeVariation;
public Variation? ActiveVariation
{
get => _activeVariation;
private set
{
_activeVariation = value;
// Activating a single variation collapses any live cross-fade back to it.
ResetBlendWeights(value?.Id ?? Guid.Empty);
}
}
///
/// True while a [BlendSnapshots] operator is procedurally driving this pool's blend. Manual
/// blend faders treat the pool as read-only while this is set so the two don't fight.
///
public bool IsBlendDrivenByOperator { get; set; }
// Live cross-fade weight vector (session-only): variationId → weight. Persists across picker
// opens, resets to the single active variation on activation, drops entries on removal.
private readonly Dictionary _blendWeights = new();
private readonly Dictionary _dragStartWeights = new();
private readonly List _blendWeightKeyBuffer = new();
private readonly List _weightedBlendVariations = new();
private readonly List _weightedBlendWeights = new();
public SymbolVariationPool(Guid symbolId)
{
SymbolId = symbolId;
ResolveVariationFilePaths(symbolId, out var writableFilePath, out var readOnlyDefaultsFilePath);
_userVariations = LoadVariationsFromFile(writableFilePath, symbolId);
_defaults = readOnlyDefaultsFilePath != null ? LoadVariationsFromFile(readOnlyDefaultsFilePath, symbolId) : [];
_allVariations = new List(_userVariations.Count + _defaults.Count);
_allVariations.AddRange(_userVariations);
_allVariations.AddRange(_defaults);
UserVariations = _userVariations;
Defaults = _defaults;
AllVariations = _allVariations;
DeduplicateSnapshotActivationIndices();
}
///
/// The ActivationIndex is both the MIDI/controller slot and the snapshot sort order, so it must be
/// unique per snapshot. Old files (or hand edits) can hold clashing or negative indices; reassign
/// those to the lowest free slots on load so the list, grid and pads stay coherent. In-memory only —
/// the fix is written out by the next save.
///
private void DeduplicateSnapshotActivationIndices()
{
var used = new HashSet();
List? clashing = null;
foreach (var v in _allVariations)
{
if (!v.IsSnapshot)
continue;
if (v.ActivationIndex >= 0 && used.Add(v.ActivationIndex))
continue;
(clashing ??= new List()).Add(v);
}
if (clashing == null)
return;
var nextFree = 0;
foreach (var v in clashing)
{
while (!used.Add(nextFree))
nextFree++;
Log.Warning($"Snapshot '{v.Title ?? "(unnamed)"}' had a clashing controller index {v.ActivationIndex}; reassigned to {nextFree} to keep the snapshot order unique.");
v.ActivationIndex = nextFree;
}
}
#region serialization
///
/// Variations live in the owning package's .meta/Variations folder. For read-only packages
/// (e.g. Lib in standalone builds) that folder only provides the shipped defaults, and user
/// created variations are kept in a per-user overlay folder in AppData instead.
/// Paths are resolved on every load/save because symbols can move between packages.
///
private static void ResolveVariationFilePaths(Guid symbolId, out string writableFilePath, out string? readOnlyDefaultsFilePath)
{
readOnlyDefaultsFilePath = null;
if (SymbolUiRegistry.TryGetSymbolUi(symbolId, out var symbolUi))
{
var package = symbolUi.Symbol.SymbolPackage;
var packageFilePath = GetVariationFilePathInPackage(package.Folder, symbolId);
if (!package.IsReadOnly)
{
writableFilePath = packageFilePath;
return;
}
readOnlyDefaultsFilePath = packageFilePath;
}
writableFilePath = GetVariationFilePathInUserOverlay(symbolId);
}
internal static string GetVariationFilePathInPackage(string packageFolder, Guid symbolId)
=> Path.Combine(packageFolder, FileLocations.MetaSubFolder, VariationsSubFolder, $"{symbolId}.var");
internal static string GetVariationFilePathInUserOverlay(Guid symbolId)
=> Path.Combine(FileLocations.SettingsDirectory, UserOverlaySubFolder, $"{symbolId}.var");
internal static List LoadVariationsFromFile(string filePath, Guid compositionId)
{
string fileContent;
try
{
if (!File.Exists(filePath))
return [];
fileContent = File.ReadAllText(filePath);
}
catch (Exception e)
{
Log.Warning($"Failed to read variations file {filePath}: {e.Message}");
return [];
}
using var sr = new StringReader(fileContent);
using var jsonReader = new JsonTextReader(sr);
var result = new List();
try
{
var jToken = JToken.ReadFrom(jsonReader, SymbolJson.LoadSettings);
if (jToken["Variations"] is JArray jArray)
{
foreach (var sceneToken in jArray)
{
if (!sceneToken.Any())
{
Log.Error("No variations?");
continue;
}
if (!Variation.TryLoadVariationFromJson(compositionId, sceneToken, out var newVariation))
{
Log.Warning("Failed to parse variation json:" + sceneToken);
continue;
}
//TODO: this needs to be implemented
//newVariation.IsPreset = true;
result.Add(newVariation);
}
}
}
catch (Exception e)
{
Log.Error($"Failed to load presets and variations for {compositionId}: {e.Message}");
return [];
}
return result;
}
public void SaveVariationsToFile()
{
ResolveVariationFilePaths(SymbolId, out var writableFilePath, out _);
if (!TrySaveVariationsToFile(writableFilePath, SymbolId, UserVariations))
Log.Error($"Failed to save presets and variations for {SymbolId}");
}
internal static bool TrySaveVariationsToFile(string filePath, Guid symbolId, IReadOnlyList variations)
{
using var sw = new StringWriter();
using var writer = new JsonTextWriter(sw);
try
{
writer.Formatting = Formatting.Indented;
writer.WriteStartObject();
writer.WriteValue("Id", symbolId);
writer.WritePropertyName("Variations");
writer.WriteStartArray();
foreach (var variation in variations)
{
variation.ToJson(writer);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
catch (Exception e)
{
Log.Error($"Json variation serialization failed: {e.Message}");
return false;
}
try
{
var directory = Path.GetDirectoryName(filePath);
if (directory != null)
Directory.CreateDirectory(directory);
File.WriteAllText(filePath, sw.ToString());
return true;
}
catch (Exception e)
{
Log.Error($"Failed to save variations file {filePath}: {e.Message}");
return false;
}
}
internal const string VariationsSubFolder = "Variations";
/** Pre-4.2 location of all variation files; still the writable location for symbols of read-only packages. */
internal const string UserOverlaySubFolder = "variations";
#endregion
public void Apply(Instance instance, Variation variation)
{
StopHover();
ActiveVariation = variation;
MacroCommand? newCommand;
if (variation.IsPreset)
{
if (!TryCreateApplyPresetCommand(instance, variation, out newCommand))
return;
}
else
{
if (!TryCreateApplyVariationCommand(instance, variation, out newCommand))
return;
}
UpdateActiveStateForVariation(variation.ActivationIndex);
UndoRedoStack.AddAndExecute(newCommand);
}
///
/// Applies a variation's values like but without recording an undo step. Used by
/// procedural drivers (e.g. the [ActivateSnapshot] operator): each activation is automation output,
/// not a user edit that belongs on the undo stack. The values still persist as the current state.
///
public void ApplyWithoutUndo(Instance instance, Variation variation)
{
StopHover();
ActiveVariation = variation;
MacroCommand? newCommand;
if (variation.IsPreset)
{
if (!TryCreateApplyPresetCommand(instance, variation, out newCommand))
return;
}
else
{
if (!TryCreateApplyVariationCommand(instance, variation, out newCommand))
return;
}
UpdateActiveStateForVariation(variation.ActivationIndex);
newCommand.Do();
}
///
/// Marks a variation as the active one without applying its values — used when a freshly
/// created snapshot already equals the current state, so applying would be a no-op command.
///
public void SetActiveVariationWithoutApply(Variation variation)
{
ActiveVariation = variation;
UpdateActiveStateForVariation(variation.ActivationIndex);
}
public void UpdateActiveStateForVariation(int variationIndex)
{
foreach (var v in AllVariations)
{
v.State = v.ActivationIndex == variationIndex ? Variation.States.Active : Variation.States.InActive;
}
//variation.State = Variation.States.Active;
}
#region live blend weights
/// Current live cross-fade weight for a variation (0 when not part of the mix).
public float GetBlendWeight(Guid variationId) => _blendWeights.GetValueOrDefault(variationId, 0f);
public bool HasBlendWeights => _blendWeights.Count > 0;
///
/// True when the live weight vector is a genuine mix of 2+ variations — i.e. not the resting
/// single-active state — so it's worth annotating (e.g. on the Variations thumbnails).
///
public bool IsLiveBlendMix
{
get
{
var count = 0;
foreach (var v in _blendWeights.Values)
{
if (v > 0.001f && ++count > 1)
return true;
}
return false;
}
}
///
/// True when this variation holds the entire weight (the lone 100%) — its fader can't be reduced
/// directly because there's nowhere to hand the weight to; another fader has to be raised first.
///
public bool IsSoleFullWeight(Guid variationId)
{
if (GetBlendWeight(variationId) < 0.999f)
return false;
foreach (var (k, v) in _blendWeights)
{
if (k != variationId && v > 0.001f)
return false;
}
return true;
}
/// Collapse the live weight vector to a single fully-active variation (empty clears it).
public void ResetBlendWeights(Guid activeVariationId)
{
_blendWeights.Clear();
if (activeVariationId != Guid.Empty)
_blendWeights[activeVariationId] = 1f;
}
///
/// Snapshot the weight vector at the start of a fader drag. The dragged fader then cross-fades
/// against these starting ratios, so it can be dragged to 100% and back without losing the
/// sources it faded out.
///
public void BeginBlendWeightDrag()
{
_dragStartWeights.Clear();
foreach (var (k, v) in _blendWeights)
_dragStartWeights[k] = v;
}
/// True if this variation was a source (non-zero) at the start of the current drag.
public bool IsDragSource(Guid variationId)
=> _dragStartWeights.TryGetValue(variationId, out var w) && w > 0.001f;
///
/// Sets one variation's cross-fade weight. Normalized (the rest rebalance proportionally so the
/// vector sums to 1) unless drops the clamp, leaving weights independent
/// and free to exceed 1. The sole non-zero weight can't be reduced — there's nowhere to send it.
///
public void SetBlendWeight(Guid variationId, float newWeight, bool free)
{
if (free)
{
_blendWeights[variationId] = newWeight < 0 ? 0 : newWeight;
return;
}
newWeight = newWeight < 0 ? 0 : (newWeight > 1 ? 1 : newWeight);
// Cross-fade against the *other faders' ratio at the start of this drag* (captured by
// BeginBlendWeightDrag), not their current values — so dragging to 100% and back refills the
// sources instead of stranding them at 0.
var sumStartOthers = 0f;
foreach (var (k, v) in _dragStartWeights)
{
if (k != variationId)
sumStartOthers += v;
}
if (sumStartOthers <= 0.0001f)
{
if (_dragStartWeights.GetValueOrDefault(variationId, 0f) < 0.999f)
{
// Nothing else in the mix and this fader wasn't full at drag start — the remainder
// is the current (unsaved) parameter state, so the weight scales freely from it.
_blendWeights.Clear();
if (newWeight > 0.0001f)
_blendWeights[variationId] = newWeight;
return;
}
// No source to fade back to — keep the lone fader full.
_blendWeights.Clear();
_blendWeights[variationId] = 1f;
return;
}
var budget = 1f - newWeight;
_blendWeights.Clear();
foreach (var (k, v) in _dragStartWeights)
{
if (k == variationId || v <= 0.0001f)
continue;
var scaled = v / sumStartOthers * budget;
if (scaled > 0.0001f)
_blendWeights[k] = scaled;
}
if (newWeight > 0.0001f)
_blendWeights[variationId] = newWeight;
}
/// The variation carrying the largest live weight, or null when the vector is empty.
public Variation? GetDominantBlendVariation()
{
var bestId = Guid.Empty;
var best = 0f;
foreach (var (k, v) in _blendWeights)
{
if (v > best)
{
best = v;
bestId = k;
}
}
if (bestId == Guid.Empty)
return null;
foreach (var v in _allVariations)
{
if (v.Id == bestId)
return v;
}
return null;
}
///
/// Applies the current weight vector as a live weighted blend (preview). Call
/// to bake it onto the undo stack.
///
public void ApplyBlendWeights(Instance instance)
{
_weightedBlendVariations.Clear();
_weightedBlendWeights.Clear();
foreach (var v in _allVariations)
{
var w = GetBlendWeight(v.Id);
if (w > 0.0001f)
{
_weightedBlendVariations.Add(v);
_weightedBlendWeights.Add(w);
}
}
if (_weightedBlendVariations.Count == 0)
{
// Dragged back to zero while blending from the current state — restore it.
StopHover();
return;
}
// A single partial weight means the remainder of the mix is the current (unsaved)
// parameter state: blend from it toward the variation instead of applying it fully.
if (_weightedBlendVariations.Count == 1 && _weightedBlendWeights[0] < 0.999f)
{
var variation = _weightedBlendVariations[0];
if (variation.IsPreset)
{
BeginBlendToPresent(instance, variation, _weightedBlendWeights[0]);
}
else
{
BeginBlendTowardsSnapshot(instance, variation, _weightedBlendWeights[0]);
}
return;
}
BeginWeightedBlend(instance, _weightedBlendVariations, _weightedBlendWeights);
}
///
/// Drives the live cross-fade from a [BlendSnapshots] operator: replaces the weight vector with the
/// given normalized weights — so the picker faders and thumbnails reflect the procedural mix — and
/// applies the blend.
///
public void ApplyOperatorDrivenBlend(Instance instance, List variations, List normalizedWeights)
{
_blendWeights.Clear();
for (var i = 0; i < variations.Count && i < normalizedWeights.Count; i++)
_blendWeights[variations[i].Id] = normalizedWeights[i];
ApplyBlendWeights(instance);
}
private void DropBlendWeight(Guid variationId)
{
if (!_blendWeights.Remove(variationId))
return;
// Renormalize the remainder so the cross-fade still sums to 1.
var sum = 0f;
foreach (var v in _blendWeights.Values)
sum += v;
if (sum <= 0.0001f)
return;
_blendWeightKeyBuffer.Clear();
foreach (var k in _blendWeights.Keys)
_blendWeightKeyBuffer.Add(k);
foreach (var k in _blendWeightKeyBuffer)
_blendWeights[k] /= sum;
}
#endregion
public void BeginHover(Instance instance, Variation variation)
{
StopHover();
MacroCommand? newCommand;
if (variation.IsPreset)
{
if (!TryCreateApplyPresetCommand(instance, variation, out newCommand))
return;
}
else
{
if (!TryCreateApplyVariationCommand(instance, variation, out newCommand))
return;
}
RememberModificationFlagForPreview(instance, variation.IsPreset);
_activeBlendCommand = newCommand;
newCommand.Do();
}
public void BeginBlendToPresent(Instance instance, Variation variation, float blend)
{
StopHover();
if (!TryCreateBlendToPresetCommand(instance, variation, blend, out var macroCommand))
return;
RememberModificationFlagForPreview(instance, isPreset: true);
_activeBlendCommand = macroCommand;
_activeBlendCommand.Do();
}
public void BeginBlendTowardsSnapshot(Instance instance, Variation variation, float blend)
{
StopHover();
if (TryCreateBlendTowardsVariationCommand(instance, variation, blend, out var newMacroCommand))
{
RememberModificationFlagForPreview(instance, isPreset: false);
_activeBlendCommand = newMacroCommand;
_activeBlendCommand.Do();
}
// we dont need this
//UpdateActiveStateForVariation(variation.ActivationIndex);
}
public void BeginWeightedBlend(Instance instance, List variations, IEnumerable weights)
{
StopHover();
if (variations.Count == 0)
return;
var countPresets = 0;
var countSnapshots = 0;
foreach (var s in variations)
{
if (s.IsPreset)
{
countPresets++;
}
else
{
countSnapshots++;
}
}
if (countSnapshots == variations.Count && countPresets == 0)
{
RememberModificationFlagForPreview(instance, isPreset: false);
_activeBlendCommand = CreateWeightedBlendSnapshotCommand(instance, variations, weights);
_activeBlendCommand?.Do();
}
else if (countPresets == variations.Count && countSnapshots == 0)
{
if (CreateWeightedBlendPresetCommand(instance, variations, weights, out var newMacroCommand))
{
RememberModificationFlagForPreview(instance, isPreset: true);
_activeBlendCommand = newMacroCommand;
newMacroCommand.Do();
}
else
{
_activeBlendCommand = null;
}
}
else
{
Log.Error($"Can't mix {countPresets} presets and {countSnapshots} snapshots for weighted blending.");
}
}
public void ApplyCurrentBlend()
{
if (_activeBlendCommand != null)
UndoRedoStack.Add(_activeBlendCommand);
_activeBlendCommand = null;
_cleanSymbolIdBeforePreview = Guid.Empty;
}
public void StopHover()
{
if (_activeBlendCommand == null)
{
return;
}
_activeBlendCommand.Undo();
_activeBlendCommand = null;
RestoreModificationFlagAfterPreview();
}
///
/// Save non-default parameters of single selected InstanceAccess as preset for its Symbol.
///
public Variation CreatePresetForInstanceSymbol(Instance instance)
{
var changes = new Dictionary();
var symbolUi = instance.Symbol.GetSymbolUi();
foreach (var input in instance.Inputs)
{
if (input.Input.IsDefault)
{
continue;
}
if(symbolUi.InputUis.TryGetValue(input.Id, out var inputUi))
{
if (inputUi.ExcludedFromPresets)
{
Log.Debug($"Exclude {symbolUi}.{inputUi.InputDefinition.Name} for preset.");
continue;
}
}
if (ValueUtils.BlendMethods.ContainsKey(input.Input.Value.ValueType))
{
changes[input.Id] = input.Input.Value.Clone();
}
}
var newVariation = new Variation
{
Id = Guid.NewGuid(),
Title = "untitled",
ActivationIndex = AllVariations.Count + 1, //TODO: First find the highest activation index
IsPreset = true,
PublishedDate = DateTime.Now,
ParameterSetsForChildIds = new Dictionary>
{
[Guid.Empty] = changes
},
};
var command = new AddPresetOrVariationCommand(instance.Symbol, newVariation);
UndoRedoStack.AddAndExecute(command);
//SaveVariationsToFile();
return newVariation;
}
public bool TryCreateVariationForCompositionInstances(List instances, [NotNullWhen(true)] out Variation? newVariation)
//public Variation CreateVariationForCompositionInstances(List instances)
{
newVariation = null;
var changeSets = new Dictionary>();
if (instances == null! || instances.Count == 0)
{
Log.Warning("No instances to create variation for");
return false;
}
Symbol? parentSymbol = null;
foreach (var instance in instances)
{
if (instance.Parent == null || instance.Parent.Symbol.Id != SymbolId)
{
Log.Error($"InstanceAccess {instance.SymbolChildId} is not a child of VariationPool operator {SymbolId}");
return false;
}
parentSymbol = instance.Parent.Symbol;
var childUi = instance.GetChildUi();
var changeSet = new Dictionary();
var hasAnimatableParameters = false;
foreach (var input in instance.Inputs)
{
if (!ValueUtils.BlendMethods.ContainsKey(input.Input.Value.ValueType))
continue;
if (childUi != null && !childUi.IsInputIncludedForVariation(input.Id))
continue;
hasAnimatableParameters = true;
if (input.Input.IsDefault)
{
continue;
}
changeSet[input.Id] = input.Input.Value.Clone();
}
if (!hasAnimatableParameters)
continue;
changeSets[instance.SymbolChildId] = changeSet;
}
newVariation = new Variation
{
Id = Guid.NewGuid(),
Title = "untitled",
ActivationIndex = AllVariations.Count + 1, //TODO: First find the highest activation index
IsPreset = false,
PublishedDate = DateTime.Now,
ParameterSetsForChildIds = changeSets,
};
var command = new AddPresetOrVariationCommand(parentSymbol, newVariation);
UndoRedoStack.AddAndExecute(command);
SaveVariationsToFile();
return true;
}
///
/// The lowest controller index above not used by a snapshot.
/// skips one variation (e.g. the freshly-created one being placed).
///
public int GetNextFreeActivationIndexAfter(int afterIndex, Variation? ignore = null)
{
var index = afterIndex + 1;
while (IsActivationIndexUsed(index, ignore))
index++;
return index;
}
private bool IsActivationIndexUsed(int activationIndex, Variation? ignore)
{
foreach (var v in _allVariations)
{
if (v == ignore || v.IsPreset)
continue;
if (v.ActivationIndex == activationIndex)
return true;
}
return false;
}
public void UpdateVariationPropertiesForInstances(Variation variation, List instances, MacroCommand? collectInto = null)
{
if (instances == null! || instances.Count == 0)
{
Log.Warning("No instances to create variation for");
return;
}
var newParameterSets = new Dictionary>(variation.ParameterSetsForChildIds);
foreach (var instance in instances)
{
Debug.Assert(instance.Parent != null);
if (instance.Parent.Symbol.Id != SymbolId)
{
Log.Error($"Instance {instance.SymbolChildId} is not a child of VariationPool operator {SymbolId}");
return;
}
var childUi = instance.GetChildUi();
var changeSet = new Dictionary();
var hasAnimatableParameters = false;
foreach (var input in instance.Inputs)
{
if (!ValueUtils.BlendMethods.ContainsKey(input.Input.Value.ValueType))
continue;
if (childUi != null && !childUi.IsInputIncludedForVariation(input.Id))
continue;
hasAnimatableParameters = true;
if (input.Input.IsDefault)
{
continue;
}
changeSet[input.Id] = input.Input.Value.Clone();
}
if (!hasAnimatableParameters)
continue;
newParameterSets[instance.SymbolChildId] = changeSet;
}
var command = new UpdateVariationParametersCommand(this, variation, newParameterSets);
if (collectInto != null)
collectInto.AddAndExecCommand(command);
else
UndoRedoStack.AddAndExecute(command);
}
public void DeleteVariation(Variation variation)
{
var command = new DeleteVariationCommand(this, variation);
UndoRedoStack.AddAndExecute(command);
SaveVariationsToFile();
}
public void DeleteVariations(List variations)
{
var commands = new List();
foreach (var variation in variations)
{
commands.Add(new DeleteVariationCommand(this, variation));
}
var newCommand = new MacroCommand("Delete variations", commands);
UndoRedoStack.AddAndExecute(newCommand);
SaveVariationsToFile();
}
private static bool TryCreateApplyVariationCommand(Instance compositionInstance, Variation variation, [NotNullWhen(true)] out MacroCommand? newMacroCommand)
{
newMacroCommand = null;
var commands = new List();
var compositionSymbol = compositionInstance.Symbol;
foreach (var (childId, parameterSets) in variation.ParameterSetsForChildIds)
{
if (childId == Guid.Empty)
{
Log.Warning("Didn't expect parent-reference id in variation");
continue;
}
if (!compositionInstance.Children.TryGetChildInstance(childId, out var instance))
continue;
var symbolChild = instance.SymbolChild;
var childUi = instance.GetChildUi();
// SymbolChild would only be null if the instance has no parent - this would only ever happen if the composition
// erroneously has a non-child instance in its children list
foreach (var input in symbolChild!.Inputs.Values)
{
if (!ValueUtils.BlendMethods.TryGetValue(input.Value.ValueType, out _))
continue;
if (parameterSets.TryGetValue(input.Id, out var param))
{
// if (param == null)
// continue;
var newCommand = new ChangeInputValueCommand(compositionSymbol, childId, input, param);
commands.Add(newCommand);
}
else if (childUi == null || childUi.IsInputIncludedForVariation(input.Id))
{
// Reset non-defaults, but leave parameters alone that are not snapshot-controlled
commands.Add(new ResetInputToDefault(compositionSymbol, childId, input));
}
}
}
newMacroCommand = new MacroCommand("Apply Variation Values", commands);
return true;
}
private static MacroCommand CreateWeightedBlendSnapshotCommand(Instance compositionInstance, List variations, IEnumerable weights)
{
var commands = new List();
var parentSymbol = compositionInstance.Symbol;
var weightsArray = weights.ToArray();
// Collect instances
var affectedInstances = new HashSet();
foreach (var v in variations)
{
affectedInstances.UnionWith(v.ParameterSetsForChildIds.Keys);
}
foreach (var childId2 in affectedInstances)
{
if (!compositionInstance.Children.TryGetChildInstance(childId2, out var instance))
continue;
// Collect variation parameters
var variationParameterSets = new List>();
foreach (var variation in variations)
{
if (variation.ParameterSetsForChildIds.TryGetValue(childId2, out var parameterSet))
{
variationParameterSets.Add(parameterSet);
}
}
foreach (var inputSlot in instance.Inputs)
{
if (!ValueUtils.WeightedBlendMethods.TryGetValue(inputSlot.Input.DefaultValue.ValueType, out var blendFunction))
continue;
var values = new List();
var definedForSome = false;
foreach (var parametersForInputs in variationParameterSets)
{
if (parametersForInputs.TryGetValue(inputSlot.Id, out var paramValue))
{
values.Add(paramValue);
definedForSome = true;
}
else
{
values.Add(inputSlot.Input.DefaultValue);
}
}
if (definedForSome && weightsArray.Length == values.Count)
{
var mixed2 = blendFunction(values.ToArray(), weightsArray);
var newCommand = new ChangeInputValueCommand(parentSymbol, instance.SymbolChildId, inputSlot.Input, mixed2);
commands.Add(newCommand);
}
}
}
var activeBlendCommand = new MacroCommand("Set Blended Snapshot Values", commands);
return activeBlendCommand;
}
private static bool TryCreateBlendTowardsVariationCommand(Instance compositionInstance, Variation variation, float blend,
[NotNullWhen(true)] out MacroCommand? newMacroCommand)
{
newMacroCommand = null;
var commands = new List();
if (!variation.IsSnapshot)
return false;
foreach (var child in compositionInstance.Children.Values)
{
if (!variation.ParameterSetsForChildIds.TryGetValue(child.SymbolChildId, out var parametersForInputs))
continue;
var childUi = child.GetChildUi();
foreach (var inputSlot in child.Inputs)
{
if (!ValueUtils.BlendMethods.TryGetValue(inputSlot.Input.DefaultValue.ValueType, out var blendFunction))
continue;
if (parametersForInputs.TryGetValue(inputSlot.Id, out var parameter))
{
var mixed = blendFunction(inputSlot.Input.Value, parameter, blend);
var newCommand = new ChangeInputValueCommand(compositionInstance.Symbol, child.SymbolChildId, inputSlot.Input, mixed);
commands.Add(newCommand);
}
else if (!inputSlot.Input.IsDefault
&& (childUi == null || childUi.IsInputIncludedForVariation(inputSlot.Id)))
{
var mixed = blendFunction(inputSlot.Input.Value, inputSlot.Input.DefaultValue, blend);
var newCommand = new ChangeInputValueCommand(compositionInstance.Symbol, child.SymbolChildId, inputSlot.Input, mixed);
commands.Add(newCommand);
}
}
}
newMacroCommand = new MacroCommand("Blend towards snapshot", commands);
return true;
}
private static bool TryCreateApplyPresetCommand(Instance instance, Variation variation, [NotNullWhen(true)] out MacroCommand? newMacroCommand)
{
newMacroCommand = null;
if (instance.Parent == null)
return false;
const string commandName = "Apply Preset Values";
if (!instance.Parent.Symbol.Children.ContainsKey(instance.SymbolChildId))
{
newMacroCommand = new MacroCommand(commandName, Array.Empty());
return true;
}
var commands = new List();
foreach (var (childId, parametersForInputs) in variation.ParameterSetsForChildIds)
{
if (childId != Guid.Empty)
{
Log.Warning("Didn't expect childId in preset");
continue;
}
foreach (var inputSlot in instance.Inputs)
{
if (!ValueUtils.BlendMethods.ContainsKey(inputSlot.ValueType))
continue;
if (parametersForInputs.TryGetValue(inputSlot.Id, out var paramValue))
{
var newCommand = new ChangeInputValueCommand(instance.Parent.Symbol, instance.SymbolChildId, inputSlot.Input, paramValue);
commands.Add(newCommand);
}
else
{
// ResetOtherNonDefaults
commands.Add(new ResetInputToDefault(instance.Parent.Symbol, instance.SymbolChildId, inputSlot.Input));
}
}
}
newMacroCommand = new MacroCommand(commandName, commands);
return true;
}
private static bool TryCreateBlendToPresetCommand(Instance instance, Variation variation, float blend,
[NotNullWhen(true)] out MacroCommand? newMacroCommand)
{
newMacroCommand = null;
var commands = new List();
if (instance.Parent == null)
return false;
var parentSymbol = instance.Parent.Symbol;
if (parentSymbol.Children.ContainsKey(instance.SymbolChildId))
{
foreach (var (childId, parametersForInputs) in variation.ParameterSetsForChildIds)
{
if (childId != Guid.Empty)
{
Log.Warning("Didn't expect childId in preset");
continue;
}
foreach (var inputSlot in instance.Inputs)
{
if (!ValueUtils.BlendMethods.TryGetValue(inputSlot.Input.DefaultValue.ValueType, out var blendFunction))
continue;
if (parametersForInputs.TryGetValue(inputSlot.Id, out var parameter))
{
// TODO:
if (parameter == null)
continue;
var mixed = blendFunction(inputSlot.Input.Value, parameter, blend);
var newCommand = new ChangeInputValueCommand(parentSymbol, instance.SymbolChildId, inputSlot.Input, mixed);
commands.Add(newCommand);
}
else if (!inputSlot.Input.IsDefault)
{
var mixed = blendFunction(inputSlot.Input.Value, inputSlot.Input.DefaultValue, blend);
var newCommand = new ChangeInputValueCommand(parentSymbol, instance.SymbolChildId, inputSlot.Input, mixed);
commands.Add(newCommand);
}
}
}
}
newMacroCommand = new MacroCommand("Set Preset Values", commands);
return true;
}
private static bool CreateWeightedBlendPresetCommand(Instance instance, List variations, IEnumerable weights,
[NotNullWhen(true)] out MacroCommand? newMacroCommand)
{
newMacroCommand = null;
var commands = new List();
if (instance.Parent == null)
return false;
var parentSymbol = instance.Parent.Symbol;
var weightsArray = weights.ToArray();
if (parentSymbol.Children.ContainsKey(instance.SymbolChildId))
{
// collect variation parameters
var variationParameterSets = new List>();
foreach (var variation in variations)
{
foreach (var (childId, parametersForInputs) in variation.ParameterSetsForChildIds)
{
if (childId != Guid.Empty)
{
Log.Warning("Didn't expect childId in preset");
continue;
}
variationParameterSets.Add(parametersForInputs);
}
}
foreach (var inputSlot in instance.Inputs)
{
if (!ValueUtils.WeightedBlendMethods.TryGetValue(inputSlot.Input.DefaultValue.ValueType, out var blendFunction))
continue;
var values = new List();
var definedForSome = false;
foreach (var parametersForInputs in variationParameterSets)
{
if (parametersForInputs.TryGetValue(inputSlot.Id, out var parameterValue))
{
// if (parameterValue == null)
// continue;
values.Add(parameterValue);
definedForSome = true;
}
else
{
values.Add(inputSlot.Input.DefaultValue);
}
}
if (definedForSome && weightsArray.Length == values.Count)
{
var mixed2 = blendFunction(values.ToArray(), weightsArray);
var newCommand = new ChangeInputValueCommand(parentSymbol, instance.SymbolChildId, inputSlot.Input, mixed2);
commands.Add(newCommand);
}
}
}
newMacroCommand = new MacroCommand("Set Blended Preset Values", commands);
return true;
}
/// True when the instance's current parameter values still match the preset's stored values.
public static bool DoesInstanceMatchPreset(Instance instance, Variation preset)
=> DoesPresetVariationMatch(preset, instance) != MatchTypes.NoMatch;
///
/// True when the preset fully describes the current state: its stored values match AND every other
/// preset-eligible parameter is at default. Stricter than ,
/// which tolerates additional non-default parameters.
///
public static bool DoesInstanceMatchPresetExactly(Instance instance, Variation preset)
=> DoesPresetVariationMatch(preset, instance) == MatchTypes.PresetAndDefaultParamsMatch;
private static MatchTypes DoesPresetVariationMatch(Variation variation, Instance instance)
{
var setCorrectly = true;
var foundOneMatch = false;
var foundUnknownNonDefaults = false;
var symbolUi = instance.Symbol.GetSymbolUi();
foreach (var (symbolChildId, values) in variation.ParameterSetsForChildIds)
{
if (symbolChildId != Guid.Empty)
continue;
foreach (var input in instance.Inputs)
{
var inputIsDefault = input.Input.IsDefault;
var variationIncludesInput = values.ContainsKey(input.Id);
if (!ValueUtils.CompareFunctions.TryGetValue(input.ValueType, out var function))
continue;
if (variationIncludesInput)
{
foundOneMatch = true;
if (inputIsDefault)
{
setCorrectly = false;
}
else
{
var inputValueMatches = function(values[input.Id], input.Input.Value);
setCorrectly &= inputValueMatches;
}
}
else
{
if (inputIsDefault)
continue;
// Mirror the eligibility filter of CreatePresetForInstanceSymbol: inputs that can
// never be stored in a preset must not count as drift.
if (!ValueUtils.BlendMethods.ContainsKey(input.ValueType))
continue;
if (symbolUi.InputUis.TryGetValue(input.Id, out var inputUi) && inputUi.ExcludedFromPresets)
continue;
foundUnknownNonDefaults = true;
}
}
}
if (!foundOneMatch || !setCorrectly)
{
return MatchTypes.NoMatch;
}
return foundUnknownNonDefaults ? MatchTypes.PresetParamsMatch : MatchTypes.PresetAndDefaultParamsMatch;
}
private enum MatchTypes
{
NoMatch,
PresetParamsMatch,
PresetAndDefaultParamsMatch,
}
public static bool TryGetSnapshot(int activationIndex, [NotNullWhen(true)] out Variation? variation)
{
variation = null;
if (VariationHandling.ActivePoolForSnapshots == null)
return false;
foreach (var v in VariationHandling.ActivePoolForSnapshots.AllVariations)
{
if (v.ActivationIndex != activationIndex)
continue;
variation = v;
return true;
}
return false;
}
public void AddUserVariation(Variation newVariation)
{
_userVariations.Add(newVariation);
_allVariations.Add(newVariation);
}
public void RemoveUserVariation(Variation newVariation)
{
_userVariations.Remove(newVariation);
_allVariations.Remove(newVariation);
DropBlendWeight(newVariation.Id);
// Keep the active reference from dangling (e.g. undoing a preset creation or deleting the active one)
if (_activeVariation == newVariation)
ActiveVariation = null;
}
///
/// Preview commands (hover, thumbnail rendering, live blends) flag the composition's SymbolUi as
/// modified on both Do and Undo. Since fully reverts the values, restore a
/// previously clean flag — otherwise merely previewing marks read-only symbols as modified and
/// triggers the save-as-copy dialog when leaving them.
///
private void RememberModificationFlagForPreview(Instance instance, bool isPreset)
{
_cleanSymbolIdBeforePreview = Guid.Empty;
var compositionSymbol = isPreset ? instance.Parent?.Symbol : instance.Symbol;
if (compositionSymbol == null)
return;
if (!SymbolUiRegistry.TryGetSymbolUi(compositionSymbol.Id, out var symbolUi))
return;
if (!symbolUi.HasBeenModified)
_cleanSymbolIdBeforePreview = compositionSymbol.Id;
}
private void RestoreModificationFlagAfterPreview()
{
if (_cleanSymbolIdBeforePreview == Guid.Empty)
return;
if (SymbolUiRegistry.TryGetSymbolUi(_cleanSymbolIdBeforePreview, out var symbolUi))
symbolUi.ClearModifiedFlag();
_cleanSymbolIdBeforePreview = Guid.Empty;
}
private MacroCommand? _activeBlendCommand;
/// Id of the composition symbol that was unmodified when the current preview began.
private Guid _cleanSymbolIdBeforePreview;
}