using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using UnityEngine; using UnityEngine.Timeline; using UnityObject = UnityEngine.Object; namespace UnityEditor.Timeline { class CurvesProxy : ICurvesOwner { public AnimationClip curves { get { return proxyCurves != null ? proxyCurves : m_OriginalOwner.curves; } } public bool hasCurves { get { return m_IsAnimatable || m_OriginalOwner.hasCurves; } } public double duration { get { return m_OriginalOwner.duration; } } public string defaultCurvesName { get { return m_OriginalOwner.defaultCurvesName; } } public UnityObject asset { get { return m_OriginalOwner.asset; } } public UnityObject assetOwner { get { return m_OriginalOwner.assetOwner; } } public TrackAsset targetTrack { get { return m_OriginalOwner.targetTrack; } } readonly ICurvesOwner m_OriginalOwner; readonly bool m_IsAnimatable; readonly Dictionary m_PropertiesMap = new Dictionary(); int m_ProxyIsRebuilding = 0; AnimationClip m_ProxyCurves; AnimationClip proxyCurves { get { if (!m_IsAnimatable) return null; if (m_ProxyCurves == null) RebuildProxyCurves(); return m_ProxyCurves; } } public CurvesProxy([NotNull] ICurvesOwner originalOwner) { m_OriginalOwner = originalOwner; m_IsAnimatable = originalOwner.HasAnyAnimatableParameters(); RebuildProxyCurves(); } public void CreateCurves(string curvesClipName) { m_OriginalOwner.CreateCurves(curvesClipName); TimelineEditor.window.state.rebuildGraph = true; } public void ConfigureCurveWrapper(CurveWrapper wrapper) { var color = CurveUtility.GetPropertyColor(wrapper.binding.propertyName); wrapper.color = color; float h, s, v; Color.RGBToHSV(color, out h, out s, out v); wrapper.wrapColorMultiplier = Color.HSVToRGB(h, s * 0.33f, v * 1.15f); var curve = AnimationUtility.GetEditorCurve(proxyCurves, wrapper.binding); wrapper.renderer = new NormalCurveRenderer(curve); // Use curve length instead of animation clip length wrapper.renderer.SetCustomRange(0.0f, curve.keys.Last().time); } public void RebuildCurves() { RebuildProxyCurves(); } public void RemoveCurves(IEnumerable bindings) { if (m_ProxyIsRebuilding > 0 || !m_OriginalOwner.hasCurves) return; Undo.RegisterCompleteObjectUndo(m_OriginalOwner.curves, L10n.Tr("Remove Clip Curve")); foreach (var binding in bindings) AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, null); m_OriginalOwner.SanitizeCurvesData(); RebuildProxyCurves(); } public void UpdateCurves(IEnumerable updatedCurves) { if (m_ProxyIsRebuilding > 0) return; Undo.RegisterCompleteObjectUndo(m_OriginalOwner.asset, L10n.Tr("Edit Clip Curve")); if (m_OriginalOwner.curves != null) Undo.RegisterCompleteObjectUndo(m_OriginalOwner.curves, L10n.Tr("Edit Clip Curve")); var requireRebuild = false; foreach (var curve in updatedCurves) { requireRebuild |= curve.curve.length == 0; UpdateCurve(curve.binding, curve.curve); } if (requireRebuild) m_OriginalOwner.SanitizeCurvesData(); AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset); } public void ApplyExternalChangesToProxy() { using (new RebuildGuard(this)) { if (m_OriginalOwner.curves == null) return; var curveInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_OriginalOwner.curves); for (int i = 0; i < curveInfo.bindings.Length; i++) { if (curveInfo.curves[i] != null && curveInfo.curves.Length != 0) { if (m_PropertiesMap.TryGetValue(curveInfo.bindings[i], out var prop) && AnimatedParameterUtility.IsParameterAnimatable(prop)) AnimationUtility.SetEditorCurve(m_ProxyCurves, curveInfo.bindings[i], curveInfo.curves[i]); } } } } void UpdateCurve(EditorCurveBinding binding, AnimationCurve curve) { ApplyConstraints(binding, curve); if (curve.length == 0) { HandleAllKeysDeleted(binding); return; } // there is no curve in the animation clip, this is a proxy curve if (IsConstantCurve(binding, curve)) HandleConstantCurveValueChanged(binding, curve); else HandleCurveUpdated(binding, curve); } bool IsConstantCurve(EditorCurveBinding binding, AnimationCurve curve) { if (curve.length != 1) return false; return m_OriginalOwner.curves == null || AnimationUtility.GetEditorCurve(m_OriginalOwner.curves, binding) == null; } void ApplyConstraints(EditorCurveBinding binding, AnimationCurve curve) { if (curve.length == 0) return; var curveUpdated = false; var property = m_PropertiesMap[binding]; if (property.propertyType == SerializedPropertyType.Boolean) { TimelineAnimationUtilities.ConstrainCurveToBooleanValues(curve); curveUpdated = true; } else { var range = AnimatedParameterUtility.GetAttributeForProperty(property); if (range != null) { TimelineAnimationUtilities.ConstrainCurveToRange(curve, range.min, range.max); curveUpdated = true; } } if (!curveUpdated) return; using (new RebuildGuard(this)) { AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, curve); } } void HandleCurveUpdated(EditorCurveBinding binding, AnimationCurve updatedCurve) { if (!m_OriginalOwner.hasCurves) CreateCurves(String.Empty); AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, updatedCurve); AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, updatedCurve); } void HandleConstantCurveValueChanged(EditorCurveBinding binding, AnimationCurve updatedCurve) { var prop = m_PropertiesMap[binding]; if (prop == null) return; Undo.RegisterCompleteObjectUndo(prop.serializedObject.targetObject, L10n.Tr("Edit Clip Curve")); prop.serializedObject.UpdateIfRequiredOrScript(); CurveEditUtility.SetFromKeyValue(prop, updatedCurve.keys[0].value); prop.serializedObject.ApplyModifiedProperties(); AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, updatedCurve); } void HandleAllKeysDeleted(EditorCurveBinding binding) { if (m_OriginalOwner.hasCurves) { // Remove curve from original asset AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, null); SetProxyCurve(m_PropertiesMap[binding], binding); } } void RebuildProxyCurves() { if (!m_IsAnimatable) return; using (new RebuildGuard(this)) { if (m_ProxyCurves == null) { m_ProxyCurves = new AnimationClip { legacy = true, name = "Constant Curves", hideFlags = HideFlags.HideAndDontSave, frameRate = m_OriginalOwner.targetTrack.timelineAsset == null ? (float)TimelineAsset.EditorSettings.kDefaultFrameRate : (float)m_OriginalOwner.targetTrack.timelineAsset.editorSettings.frameRate }; } else { m_ProxyCurves.ClearCurves(); } m_OriginalOwner.SanitizeCurvesData(); AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset); var parameters = m_OriginalOwner.GetAllAnimatableParameters().ToArray(); foreach (var param in parameters) CreateProxyCurve(param, m_ProxyCurves, m_OriginalOwner.asset, param.propertyPath); AnimationClipCurveCache.Instance.GetCurveInfo(m_ProxyCurves).dirty = true; } } // updates the just the proxied values. This can be called when the asset changes, so the proxy values are properly updated public void UpdateProxyCurves() { if (!m_IsAnimatable || m_ProxyCurves == null || m_ProxyCurves.empty) return; AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset); var parameters = m_OriginalOwner.GetAllAnimatableParameters().ToArray(); using (new RebuildGuard(this)) { if (m_OriginalOwner.hasCurves) { var bindingInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_OriginalOwner.curves); foreach (var param in parameters) { var binding = AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath); if (!bindingInfo.bindings.Contains(binding, AnimationPreviewUtilities.EditorCurveBindingComparer.Instance)) SetProxyCurve(param, AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath)); } } else { foreach (var param in parameters) SetProxyCurve(param, AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath)); } } AnimationClipCurveCache.Instance.GetCurveInfo(m_ProxyCurves).dirty = true; } void CreateProxyCurve(SerializedProperty prop, AnimationClip clip, UnityObject owner, string propertyName) { var binding = AnimatedParameterUtility.GetCurveBinding(owner, propertyName); var originalCurve = m_OriginalOwner.hasCurves ? AnimationUtility.GetEditorCurve(m_OriginalOwner.curves, binding) : null; if (originalCurve != null) { AnimationUtility.SetEditorCurve(clip, binding, originalCurve); } else { SetProxyCurve(prop, binding); } m_PropertiesMap[binding] = prop; } void SetProxyCurve(SerializedProperty prop, EditorCurveBinding binding) { var curve = new AnimationCurve(); CurveEditUtility.AddKeyFrameToCurve( curve, 0.0f, m_ProxyCurves.frameRate, CurveEditUtility.GetKeyValue(prop), prop.propertyType == SerializedPropertyType.Boolean); AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, curve); } struct RebuildGuard : IDisposable { CurvesProxy m_Owner; AnimationUtility.OnCurveWasModified m_Callback; public RebuildGuard(CurvesProxy owner) { m_Callback = AnimationUtility.onCurveWasModified; AnimationUtility.onCurveWasModified = null; m_Owner = owner; m_Owner.m_ProxyIsRebuilding++; } public void Dispose() { AnimationUtility.onCurveWasModified = m_Callback; m_Owner.m_ProxyIsRebuilding--; m_Owner = null; } } } }