You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

400 lines
14 KiB

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEditor.VFX;
using Object = UnityEngine.Object;
namespace UnityEditor.VFX.UI
{
[Serializable]
class VFXGraphUndoStack
{
public enum RestoreResult
{
None,
FullGraph,
Deltas,
};
public VFXGraphUndoStack(VFXGraph initialState)
{
m_graphUndoCursor = ScriptableObject.CreateInstance<VFXGraphUndoCursor>();
m_graphUndoCursor.hideFlags = HideFlags.HideAndDontSave;
m_undoStack = new List<IBackupState>();
m_CurrentDeltas = new BackupDeltas();
m_graphUndoCursor.index = 0;
m_undoStack.Add(new BackupGraph() { graphData = initialState.Backup() });
m_Graph = initialState;
m_NeedsFlush = false;
m_CurrentCursor = 0;
}
public void UpdateState(VFXModel model, VFXModel.InvalidationCause cause)
{
bool newGroup = m_CurrentUndoGroup != Undo.GetCurrentGroup();
if (newGroup)
{
m_CurrentDeltas = new BackupDeltas();
m_FullGraphBackup = false;
}
bool stateUpdated = false;
switch (cause)
{
case VFXModel.InvalidationCause.kParamChanged:
{
if (model is VFXSlot slot) // model not beeing a VFXSlot means it is a subgraph reporting a value change
{
AddSlotValueChange(slot);
stateUpdated = true;
}
else if (model is VFXParameter) // Cannot use fast path for VFX parameters
{
m_FullGraphBackup = true;
stateUpdated = true;
}
}
break;
case VFXModel.InvalidationCause.kUIChanged:
{
// If model is null or graph (meaning group or stickynote change) or is a VFX parameter, we cannot use fast path and have to serialize all graph
if (model == null || model is VFXGraph || model is VFXParameter)
{
m_FullGraphBackup = true;
}
else // fast path for model UI change
{
AddModelUIChange(model);
}
stateUpdated = true;
}
break;
case VFXModel.InvalidationCause.kMaterialChanged:
if (model is not VFXAbstractRenderedOutput renderedOutput)
{
Debug.LogError("Unexpected model type from kMaterialChanged: " + model);
m_FullGraphBackup = true;
}
else // fast path saving only material settings
{
AddMaterialSettingsChange(renderedOutput);
}
stateUpdated = true;
break;
case VFXModel.InvalidationCause.kStructureChanged:
case VFXModel.InvalidationCause.kSettingChanged:
case VFXModel.InvalidationCause.kSpaceChanged:
case VFXModel.InvalidationCause.kConnectionChanged:
{
m_FullGraphBackup = true;
stateUpdated = true;
}
break;
}
if (!stateUpdated)
return;
if (newGroup)
{
Undo.RecordObject(m_graphUndoCursor, string.Format("Modify VFX Graph - {0} ({1})", m_Graph?.GetResource()?.name, m_graphUndoCursor.index + 1));
m_graphUndoCursor.index = m_graphUndoCursor.index + 1;
m_CurrentUndoGroup = Undo.GetCurrentGroup();
m_CurrentCursor = m_graphUndoCursor.index;
}
m_NeedsFlush = true;
}
public void FlushState()
{
if (!m_NeedsFlush)
return;
int entriesCount = m_undoStack.Count();
if (entriesCount != m_CurrentCursor + 1)
{
if (entriesCount == m_CurrentCursor)
m_undoStack.Add(null);
else if (entriesCount > m_CurrentCursor + 1)
m_undoStack.RemoveRange(m_CurrentCursor + 1, m_undoStack.Count() - (m_CurrentCursor + 1));
else
throw new InvalidOperationException("Corrupted VFX Graph undo stack - Missing entries");
}
// Store state
if (m_FullGraphBackup)
{
m_undoStack[m_CurrentCursor] = new BackupGraph() { graphData = m_Graph.Backup() };
}
else
{
m_undoStack[m_CurrentCursor] = new BackupDeltas()
{
slotValues = m_CurrentDeltas.slotValues,
modelUI = m_CurrentDeltas.modelUI,
materialSettings = m_CurrentDeltas.materialSettings
};
}
m_NeedsFlush = false;
}
public RestoreResult RestoreState()
{
if (m_CurrentCursor == m_graphUndoCursor.index)
return RestoreResult.None;
int order = Math.Sign(m_CurrentCursor - m_graphUndoCursor.index);
bool needsRecompile = false;
do
{
if (order == 1) // undo
{
// Undoing a full graph backup, needs to go back to previous backup and replay delta changes
if (m_undoStack[m_CurrentCursor] is BackupGraph)
{
int currentRestoredCursor = m_graphUndoCursor.index;
while (!(m_undoStack[currentRestoredCursor] is BackupGraph))
--currentRestoredCursor;
while (currentRestoredCursor <= m_graphUndoCursor.index)
{
m_undoStack[currentRestoredCursor].Apply(m_Graph, true);
++currentRestoredCursor;
}
needsRecompile = true;
}
// Undoing a delta command, simply send delta notifications
else
{
m_undoStack[m_CurrentCursor].Apply(m_Graph, false);
}
}
else // order == -1 // redo
{
needsRecompile = m_undoStack[m_graphUndoCursor.index].Apply(m_Graph, false);
}
m_CurrentCursor -= order;
}
while (m_CurrentCursor != m_graphUndoCursor.index);
return needsRecompile ? RestoreResult.FullGraph : RestoreResult.Deltas;
}
public void AddSlotValueChange(VFXSlot slot)
{
if (slot != null)
{
if (m_CurrentDeltas.slotValues == null)
m_CurrentDeltas.slotValues = new Dictionary<VFXSlot, object>();
m_CurrentDeltas.slotValues[slot] = slot.value;
}
}
public void AddModelUIChange(VFXModel model)
{
if (model != null)
{
if (m_CurrentDeltas.modelUI == null)
m_CurrentDeltas.modelUI = new Dictionary<VFXModel, UIState>();
m_CurrentDeltas.modelUI[model] = new UIState
{
pos = model.position,
collapsed = model.collapsed,
superCollapsed = model.superCollapsed
};
}
}
public void AddMaterialSettingsChange(VFXAbstractRenderedOutput model)
{
if (model != null)
{
if (m_CurrentDeltas.materialSettings == null)
m_CurrentDeltas.materialSettings = new();
var sourceMaterialSettings = (VFXMaterialSerializedSettings)model.GetSettingValue("materialSettings");
if (sourceMaterialSettings != null)
{
var settings = (VFXMaterialSerializedSettings)sourceMaterialSettings.Clone();
m_CurrentDeltas.materialSettings[model] = settings;
}
}
}
struct UIState
{
public Vector2 pos;
public bool collapsed;
public bool superCollapsed;
}
interface IBackupState
{
// Apply state to graph and returns true if recompilation is needed, false otherwise
public bool Apply(VFXGraph graph, bool updateDeltas);
}
class BackupGraph : IBackupState
{
public object graphData;
public bool Apply(VFXGraph graph, bool updateDeltas)
{
graph.Restore(graphData);
return true;
}
}
class BackupDeltas : IBackupState
{
public Dictionary<VFXSlot, object> slotValues;
public Dictionary<VFXModel, UIState> modelUI;
public Dictionary<VFXAbstractRenderedOutput, VFXMaterialSerializedSettings> materialSettings;
public bool Apply(VFXGraph graph, bool updateDeltas)
{
if (slotValues != null)
foreach (var kv in slotValues)
{
kv.Key.value = updateDeltas ? kv.Value : kv.Key.value;
}
if (modelUI != null)
foreach (var kv in modelUI)
{
if (updateDeltas)
{
kv.Key.position = kv.Value.pos;
kv.Key.collapsed = kv.Value.collapsed;
kv.Key.superCollapsed = kv.Value.superCollapsed;
}
else
{
kv.Key.Invalidate(VFXModel.InvalidationCause.kUIChanged);
}
}
if (materialSettings != null)
foreach (var kv in materialSettings)
{
if (updateDeltas)
{
kv.Key.SetSettingValue("materialSettings", kv.Value.Clone());
}
var material = kv.Key.FindMaterial();
if (material && kv.Key.GetSettingValue("materialSettings") is VFXMaterialSerializedSettings currentMaterialSettings)
currentMaterialSettings.ApplyToMaterial(material);
if (!updateDeltas)
{
kv.Key.Invalidate(VFXModel.InvalidationCause.kMaterialChanged);
}
}
return false;
}
}
[SerializeField]
private VFXGraph m_Graph;
[SerializeField]
private int m_CurrentCursor;
[SerializeField]
private List<IBackupState> m_undoStack;
[SerializeField]
private VFXGraphUndoCursor m_graphUndoCursor;
private BackupDeltas m_CurrentDeltas;
private bool m_NeedsFlush;
private int m_CurrentUndoGroup = -1;
private bool m_FullGraphBackup = false;
}
partial class VFXViewController : Controller<VisualEffectResource>
{
[NonSerialized]
private bool m_reentrantUndo;
[SerializeField]
private VFXGraphUndoStack m_graphUndoStack;
public bool isReentrant => m_reentrantUndo;
private void InitializeUndoStack()
{
m_graphUndoStack = new VFXGraphUndoStack(graph);
}
private void ReleaseUndoStack()
{
m_graphUndoStack = null;
}
public void IncremenentGraphUndoRedoState(VFXModel model, VFXModel.InvalidationCause cause)
{
if (m_graphUndoStack == null || m_reentrantUndo)
return;
m_graphUndoStack.UpdateState(model, cause);
}
private void WillFlushUndoRecord()
{
if (m_graphUndoStack == null)
{
return;
}
m_graphUndoStack.FlushState();
}
private void SynchronizeUndoRedoState()
{
if (m_graphUndoStack == null)
{
return;
}
m_reentrantUndo = true;
try
{
var result = m_graphUndoStack.RestoreState();
if (result != VFXGraphUndoStack.RestoreResult.None)
{
if (result == VFXGraphUndoStack.RestoreResult.FullGraph)
{
ExpressionGraphDirty = true;
model.GetOrCreateGraph().UpdateSubAssets();
EditorUtility.SetDirty(graph);
NotifyUpdate();
}
else // deltas
{
ExpressionGraphDirty = true;
ExpressionGraphDirtyParamOnly = true;
graph.SetExpressionValueDirty();
}
}
}
finally
{
m_reentrantUndo = false;
}
}
}
}