// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using FlaxEngine; using FlaxEngine.GUI; using FlaxEditor.Actions; using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph.Actors; using FlaxEditor.CustomEditors.Elements; namespace FlaxEditor.CustomEditors.Dedicated { /// /// Custom editor for . /// /// [CustomEditor(typeof(Spline)), DefaultEditor] public class SplineEditor : ActorEditor { /// /// Basis for creating tangent manipulation types for bezier curves. /// private abstract class EditTangentOptionBase { /// /// Called when user set selected tangent mode. /// /// Current spline selected on editor viewport. /// Index of current keyframe selected on spline. public abstract void OnSetMode(Spline spline, int index); /// /// Called when user select a keyframe (spline point) of current selected spline on editor viewport. /// /// Current spline selected on editor viewport. /// Index of current keyframe selected on spline. public abstract void OnSelectKeyframe(Spline spline, int index); /// /// Called when user select a tangent of current keyframe selected from spline. /// /// Current spline selected on editor viewport. /// Index of current keyframe selected on spline. public abstract void OnSelectTangent(Spline spline, int index); /// /// Called when the tangent in from current keyframe selected from spline is moved on editor viewport. /// /// Current spline selected on editor viewport. /// Index of current keyframe selected on spline. public abstract void OnMoveTangentIn(Spline spline, int index); /// /// Called when the tangent out from current keyframe selected from spline is moved on editor viewport. /// /// Current spline selected on editor viewport. /// Current spline selected on editor viewport. public abstract void OnMoveTangentOut(Spline spline, int index); } /// /// Edit curve options manipulate the curve as free mode /// private sealed class FreeTangentMode : EditTangentOptionBase { /// public override void OnSetMode(Spline spline, int index) { if (IsLinearTangentMode(spline, index)) { SetPointSmooth(spline, index); } } /// public override void OnMoveTangentIn(Spline spline, int index) { } /// public override void OnMoveTangentOut(Spline spline, int index) { } /// public override void OnSelectKeyframe(Spline spline, int index) { } /// public override void OnSelectTangent(Spline spline, int index) { } } /// /// Edit curve options to set tangents to linear /// private sealed class LinearTangentMode : EditTangentOptionBase { /// public override void OnSetMode(Spline spline, int index) { SetKeyframeLinear(spline, index); // change the selection to tangent parent (a spline point / keyframe) SetSelectSplinePointNode(spline, index); } /// public override void OnMoveTangentIn(Spline spline, int index) { } /// public override void OnMoveTangentOut(Spline spline, int index) { } /// public override void OnSelectKeyframe(Spline spline, int index) { } /// public override void OnSelectTangent(Spline spline, int index) { } } /// /// Edit curve options to align tangents of selected spline /// private sealed class AlignedTangentMode : EditTangentOptionBase { /// public override void OnSetMode(Spline spline, int index) { SmoothIfNotAligned(spline, index); } /// public override void OnSelectKeyframe(Spline spline, int index) { SmoothIfNotAligned(spline, index); } /// public override void OnSelectTangent(Spline selectedSpline, int index) { } /// public override void OnMoveTangentIn(Spline spline, int index) { SetPointAligned(spline, index, true); } /// public override void OnMoveTangentOut(Spline spline, int index) { SetPointAligned(spline, index, false); } private void SmoothIfNotAligned(Spline spline, int index) { if (!IsAlignedTangentMode(spline, index)) { SetPointSmooth(spline, index); } } private void SetPointAligned(Spline spline, int index, bool isIn) { var keyframe = spline.GetSplineKeyframe(index); var referenceTangent = isIn ? keyframe.TangentIn : keyframe.TangentOut; var otherTangent = !isIn ? keyframe.TangentIn : keyframe.TangentOut; // inverse of reference tangent otherTangent.Translation = -referenceTangent.Translation.Normalized * otherTangent.Translation.Length; if (isIn) keyframe.TangentOut = otherTangent; if (!isIn) keyframe.TangentIn = otherTangent; spline.SetSplineKeyframe(index, keyframe); } } /// /// Edit curve options manipulate the curve setting selected point /// tangent in as smoothed but tangent out as linear /// private sealed class SmoothInTangentMode : EditTangentOptionBase { /// public override void OnSetMode(Spline spline, int index) { SetTangentSmoothIn(spline, index); SetSelectTangentIn(spline, index); } /// public override void OnMoveTangentIn(Spline spline, int index) { } /// public override void OnMoveTangentOut(Spline spline, int index) { } /// public override void OnSelectKeyframe(Spline spline, int index) { } /// public override void OnSelectTangent(Spline spline, int index) { } } /// /// Edit curve options manipulate the curve setting selected point /// tangent in as linear but tangent out as smoothed /// private sealed class SmoothOutTangentMode : EditTangentOptionBase { /// public override void OnSetMode(Spline spline, int index) { SetTangentSmoothOut(spline, index); SetSelectTangentOut(spline, index); } /// public override void OnMoveTangentIn(Spline spline, int index) { } /// public override void OnMoveTangentOut(Spline spline, int index) { } /// public override void OnSelectKeyframe(Spline spline, int index) { } /// public override void OnSelectTangent(Spline spline, int index) { } } private EditTangentOptionBase _currentTangentMode; private ButtonElement _freeTangentButton; private ButtonElement _linearTangentButton; private ButtonElement _alignedTangentButton; private ButtonElement _smoothInTangentButton; private ButtonElement _smoothOutTangentButton; private bool _tanInChanged; private bool _tanOutChanged; private Vector3 _lastTanInPos; private Vector3 _lastTanOutPos; private SplineNode.SplinePointNode _selectedPoint; private SplineNode.SplinePointNode _lastPointSelected; private SplineNode.SplinePointTangentNode _selectedTangentIn; private SplineNode.SplinePointTangentNode _selectedTangentOut; /// /// Current selected spline on editor, if has /// public Spline SelectedSpline => !Values.HasDifferentValues && Values[0] is Spline ? (Spline)Values[0] : null; private bool HasPointSelected => _selectedPoint != null; private bool HasTangentsSelected => _selectedTangentIn != null || _selectedTangentOut != null; private Color SelectedButtonColor => FlaxEngine.GUI.Style.Current.BackgroundSelected; private Color NormalButtonColor => FlaxEngine.GUI.Style.Current.BackgroundNormal; /// /// Create a Spline editor /// public SplineEditor() { _currentTangentMode = new FreeTangentMode(); } /// public override void Initialize(LayoutElementsContainer layout) { base.Initialize(layout); if (Values.HasDifferentTypes == false) { layout.Space(10); layout.Header("Selected spline point"); var selectedPointsGrid = layout.CustomContainer(); selectedPointsGrid.CustomControl.SlotsHorizontally = 3; selectedPointsGrid.CustomControl.SlotsVertically = 2; selectedPointsGrid.Control.Size *= new Float2(1, 2); _linearTangentButton = selectedPointsGrid.Button("Linear"); _freeTangentButton = selectedPointsGrid.Button("Free"); _alignedTangentButton = selectedPointsGrid.Button("Aligned"); _smoothInTangentButton = selectedPointsGrid.Button("Smooth In"); _smoothOutTangentButton = selectedPointsGrid.Button("Smooth Out"); _linearTangentButton.Button.Clicked += SetModeLinear; _freeTangentButton.Button.Clicked += SetModeFree; _alignedTangentButton.Button.Clicked += SetModeAligned; _smoothInTangentButton.Button.Clicked += SetModeSmoothIn; _smoothOutTangentButton.Button.Clicked += SetModeSmoothOut; _linearTangentButton.Button.Clicked += UpdateButtonsColors; _freeTangentButton.Button.Clicked += UpdateButtonsColors; _alignedTangentButton.Button.Clicked += UpdateButtonsColors; _smoothInTangentButton.Button.Clicked += UpdateButtonsColors; _smoothOutTangentButton.Button.Clicked += UpdateButtonsColors; layout.Header("All spline points"); var grid = layout.CustomContainer(); grid.CustomControl.SlotsHorizontally = 2; grid.CustomControl.SlotsVertically = 1; grid.Button("Set Linear Tangents").Button.Clicked += OnSetTangentsLinear; grid.Button("Set Smooth Tangents").Button.Clicked += OnSetTangentsSmooth; } } /// public override void Refresh() { base.Refresh(); UpdateSelectedPoint(); UpdateSelectedTangent(); _freeTangentButton.Button.Enabled = CanSetTangentFree(); _linearTangentButton.Button.Enabled = CanSetTangentMode(); _alignedTangentButton.Button.Enabled = CanSetTangentAligned(); _smoothInTangentButton.Button.Enabled = CanSetTangentSmoothIn(); _smoothOutTangentButton.Button.Enabled = CanSetTangentSmoothOut(); if (!CanSetTangentMode()) { return; } var index = _lastPointSelected.Index; var currentTangentInPosition = SelectedSpline.GetSplineLocalTangent(index, true).Translation; var currentTangentOutPosition = SelectedSpline.GetSplineLocalTangent(index, false).Translation; if (_selectedTangentIn != null) { _tanInChanged = _lastTanInPos != currentTangentInPosition; _lastTanInPos = currentTangentInPosition; } if (_selectedTangentOut != null) { _tanOutChanged = _lastTanOutPos != currentTangentOutPosition; _lastTanOutPos = currentTangentOutPosition; } if (_tanInChanged) _currentTangentMode.OnMoveTangentIn(SelectedSpline, index); if (_tanOutChanged) _currentTangentMode.OnMoveTangentOut(SelectedSpline, index); currentTangentInPosition = SelectedSpline.GetSplineLocalTangent(index, true).Translation; currentTangentOutPosition = SelectedSpline.GetSplineLocalTangent(index, false).Translation; // update last tangents position after changes if (SelectedSpline) _lastTanInPos = currentTangentInPosition; if (SelectedSpline) _lastTanOutPos = currentTangentOutPosition; _tanInChanged = false; _tanOutChanged = false; } private bool CanSetTangentMode() { return !HasDifferentTypes && !HasDifferentValues && (HasPointSelected || HasTangentsSelected); } private bool CanSetTangentSmoothIn() { if (!CanSetTangentMode()) return false; return _lastPointSelected.Index != 0; } private bool CanSetTangentSmoothOut() { if (!CanSetTangentMode()) return false; return _lastPointSelected.Index < SelectedSpline.SplinePointsCount - 1; } private bool CanSetTangentFree() { if (!CanSetTangentMode()) return false; return _lastPointSelected.Index < SelectedSpline.SplinePointsCount - 1 && _lastPointSelected.Index != 0; } private bool CanSetTangentAligned() { if (!CanSetTangentMode()) return false; return _lastPointSelected.Index < SelectedSpline.SplinePointsCount - 1 && _lastPointSelected.Index != 0; } private void SetModeLinear() { _currentTangentMode = new LinearTangentMode(); _currentTangentMode.OnSetMode(SelectedSpline, _lastPointSelected.Index); } private void SetModeFree() { _currentTangentMode = new FreeTangentMode(); _currentTangentMode.OnSetMode(SelectedSpline, _lastPointSelected.Index); } private void SetModeAligned() { _currentTangentMode = new AlignedTangentMode(); _currentTangentMode.OnSetMode(SelectedSpline, _lastPointSelected.Index); } private void SetModeSmoothIn() { _currentTangentMode = new SmoothInTangentMode(); _currentTangentMode.OnSetMode(SelectedSpline, _lastPointSelected.Index); } private void SetModeSmoothOut() { _currentTangentMode = new SmoothOutTangentMode(); _currentTangentMode.OnSetMode(SelectedSpline, _lastPointSelected.Index); } private void UpdateSelectedPoint() { // works only if select one spline if (Editor.Instance.SceneEditing.SelectionCount != 1) { _selectedPoint = null; UpdateButtonsColors(); return; } var currentSelected = Editor.Instance.SceneEditing.Selection[0]; if (currentSelected == _selectedPoint) return; if (currentSelected is SplineNode.SplinePointNode) { _selectedPoint = currentSelected as SplineNode.SplinePointNode; _lastPointSelected = _selectedPoint; var index = _lastPointSelected.Index; SetSelectedTangentTypeAsCurrent(); UpdateButtonsColors(); _currentTangentMode.OnSelectKeyframe(SelectedSpline, index); } else { _selectedPoint = null; UpdateButtonsColors(); } } private void UpdateSelectedTangent() { // works only if select one spline if (_lastPointSelected == null || Editor.Instance.SceneEditing.SelectionCount != 1) { _selectedTangentIn = null; _selectedTangentOut = null; return; } var currentSelected = Editor.Instance.SceneEditing.Selection[0]; if (currentSelected is not SplineNode.SplinePointTangentNode) { _selectedTangentIn = null; _selectedTangentOut = null; return; } if (currentSelected == _selectedTangentIn) return; if (currentSelected == _selectedTangentOut) return; var index = _lastPointSelected.Index; if (currentSelected.Transform == SelectedSpline.GetSplineTangent(index, true)) { _selectedTangentIn = currentSelected as SplineNode.SplinePointTangentNode; _selectedTangentOut = null; _currentTangentMode.OnSelectTangent(SelectedSpline, index); return; } if (currentSelected.Transform == SelectedSpline.GetSplineTangent(index, false)) { _selectedTangentOut = currentSelected as SplineNode.SplinePointTangentNode; _selectedTangentIn = null; _currentTangentMode.OnSelectTangent(SelectedSpline, index); return; } _selectedTangentIn = null; _selectedTangentOut = null; } private void UpdateButtonsColors() { if (!CanSetTangentMode()) { _linearTangentButton.Button.BackgroundColor = NormalButtonColor; _freeTangentButton.Button.BackgroundColor = NormalButtonColor; _alignedTangentButton.Button.BackgroundColor = NormalButtonColor; _smoothInTangentButton.Button.BackgroundColor = NormalButtonColor; _smoothOutTangentButton.Button.BackgroundColor = NormalButtonColor; return; } var isFree = _currentTangentMode is FreeTangentMode; var isLinear = _currentTangentMode is LinearTangentMode; var isAligned = _currentTangentMode is AlignedTangentMode; var isSmoothIn = _currentTangentMode is SmoothInTangentMode; var isSmoothOut = _currentTangentMode is SmoothOutTangentMode; _linearTangentButton.Button.BackgroundColor = isLinear ? SelectedButtonColor : NormalButtonColor; _freeTangentButton.Button.BackgroundColor = isFree ? SelectedButtonColor : NormalButtonColor; _alignedTangentButton.Button.BackgroundColor = isAligned ? SelectedButtonColor : NormalButtonColor; _smoothInTangentButton.Button.BackgroundColor = isSmoothIn ? SelectedButtonColor : NormalButtonColor; _smoothOutTangentButton.Button.BackgroundColor = isSmoothOut ? SelectedButtonColor : NormalButtonColor; } private void SetSelectedTangentTypeAsCurrent() { var isFree = IsFreeTangentMode(SelectedSpline, _lastPointSelected.Index); var isLinear = IsLinearTangentMode(SelectedSpline, _lastPointSelected.Index); var isAligned = IsAlignedTangentMode(SelectedSpline, _lastPointSelected.Index); var isSmoothIn = IsSmoothInTangentMode(SelectedSpline, _lastPointSelected.Index); var isSmoothOut = IsSmoothOutTangentMode(SelectedSpline, _lastPointSelected.Index); if (isFree) SetModeFree(); else if (isLinear) SetModeLinear(); else if (isAligned) SetModeAligned(); else if (isSmoothIn) SetModeSmoothIn(); else if (isSmoothOut) SetModeSmoothOut(); } private void OnSetTangentsLinear() { var enableUndo = Presenter.Undo != null && Presenter.Undo.Enabled; for (int i = 0; i < Values.Count; i++) { if (Values[i] is Spline spline) { var before = enableUndo ? (BezierCurve.Keyframe[])spline.SplineKeyframes.Clone() : null; spline.SetTangentsLinear(); if (enableUndo) Presenter.Undo.AddAction(new EditSplineAction(spline, before)); SplineNode.OnSplineEdited(spline); Editor.Instance.Scene.MarkSceneEdited(spline.Scene); } } } private void OnSetTangentsSmooth() { var enableUndo = Presenter.Undo != null && Presenter.Undo.Enabled; for (int i = 0; i < Values.Count; i++) { if (Values[i] is Spline spline) { var before = enableUndo ? (BezierCurve.Keyframe[])spline.SplineKeyframes.Clone() : null; spline.SetTangentsSmooth(); if (enableUndo) Presenter.Undo.AddAction(new EditSplineAction(spline, before)); SplineNode.OnSplineEdited(spline); Editor.Instance.Scene.MarkSceneEdited(spline.Scene); } } } private static bool IsFreeTangentMode(Spline spline, int index) { if (IsLinearTangentMode(spline, index) || IsAlignedTangentMode(spline, index) || IsSmoothInTangentMode(spline, index) || IsSmoothOutTangentMode(spline, index)) { return false; } return true; } private static bool IsLinearTangentMode(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); return keyframe.TangentIn.Translation.Length == 0 && keyframe.TangentOut.Translation.Length == 0; } private static bool IsAlignedTangentMode(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); var tangentIn = keyframe.TangentIn.Translation; var tangentOut = keyframe.TangentOut.Translation; if (tangentIn.Length == 0 || tangentOut.Length == 0) { return false; } var angleBetweenTwoTangents = Vector3.Dot(tangentIn.Normalized, tangentOut.Normalized); if (angleBetweenTwoTangents < -0.99f) { return true; } return false; } private static bool IsSmoothInTangentMode(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); return keyframe.TangentIn.Translation.Length > 0 && keyframe.TangentOut.Translation.Length == 0; } private static bool IsSmoothOutTangentMode(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); return keyframe.TangentOut.Translation.Length > 0 && keyframe.TangentIn.Translation.Length == 0; } private static void SetKeyframeLinear(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); keyframe.TangentIn.Translation = Vector3.Zero; keyframe.TangentOut.Translation = Vector3.Zero; var lastSplineIndex = spline.SplinePointsCount - 1; if (index == lastSplineIndex && spline.IsLoop) { var lastPoint = spline.GetSplineKeyframe(lastSplineIndex); lastPoint.TangentIn.Translation = Vector3.Zero; lastPoint.TangentOut.Translation = Vector3.Zero; spline.SetSplineKeyframe(lastSplineIndex, lastPoint); } spline.SetSplineKeyframe(index, keyframe); } private static void SetTangentSmoothIn(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); // auto smooth tangent if's linear if (keyframe.TangentIn.Translation.Length == 0) { var previousKeyframe = spline.GetSplineKeyframe(index - 1); var tangentDirection = keyframe.Value.WorldToLocalVector(previousKeyframe.Value.Translation - keyframe.Value.Translation); tangentDirection = tangentDirection.Normalized * 100f; keyframe.TangentIn.Translation = tangentDirection; } keyframe.TangentOut.Translation = Vector3.Zero; spline.SetSplineKeyframe(index, keyframe); } private static void SetTangentSmoothOut(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); // auto smooth tangent if's linear if (keyframe.TangentOut.Translation.Length == 0) { var nextKeyframe = spline.GetSplineKeyframe(index + 1); var tangentDirection = keyframe.Value.WorldToLocalVector(nextKeyframe.Value.Translation - keyframe.Value.Translation); tangentDirection = tangentDirection.Normalized * 100f; keyframe.TangentOut.Translation = tangentDirection; } keyframe.TangentIn.Translation = Vector3.Zero; spline.SetSplineKeyframe(index, keyframe); } private static void SetPointSmooth(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); var tangentIn = keyframe.TangentIn; var tangentOut = keyframe.TangentOut; var tangentInSize = tangentIn.Translation.Length; var tangentOutSize = tangentOut.Translation.Length; var isLastKeyframe = index >= spline.SplinePointsCount - 1; var isFirstKeyframe = index <= 0; // force smooth it's linear point if (tangentInSize == 0f && !isFirstKeyframe) tangentInSize = 100; if (tangentOutSize == 0f && !isLastKeyframe) tangentOutSize = 100; var nextKeyframe = !isLastKeyframe ? spline.GetSplineKeyframe(index + 1) : keyframe; var previousKeyframe = !isFirstKeyframe ? spline.GetSplineKeyframe(index - 1) : keyframe; // calc form from Spline.cpp -> SetTangentsSmooth var slop = (keyframe.Value.Translation - previousKeyframe.Value.Translation + nextKeyframe.Value.Translation - keyframe.Value.Translation).Normalized; keyframe.TangentIn.Translation = -slop * tangentInSize; keyframe.TangentOut.Translation = slop * tangentOutSize; spline.SetSplineKeyframe(index, keyframe); } private static SplineNode.SplinePointNode GetSplinePointNode(Spline spline, int index) { return (SplineNode.SplinePointNode)SceneGraphFactory.FindNode(spline.ID).ChildNodes[index]; } private static SplineNode.SplinePointTangentNode GetSplineTangentInNode(Spline spline, int index) { var point = GetSplinePointNode(spline, index); var tangentIn = spline.GetSplineTangent(index, true); var tangentNodes = point.ChildNodes; // find tangent in node comparing all child nodes position for (int i = 0; i < tangentNodes.Count; i++) { if (tangentNodes[i].Transform.Translation == tangentIn.Translation) { return (SplineNode.SplinePointTangentNode)tangentNodes[i]; } } return null; } private static SplineNode.SplinePointTangentNode GetSplineTangentOutNode(Spline spline, int index) { var point = GetSplinePointNode(spline, index); var tangentOut = spline.GetSplineTangent(index, false); var tangentNodes = point.ChildNodes; // find tangent out node comparing all child nodes position for (int i = 0; i < tangentNodes.Count; i++) { if (tangentNodes[i].Transform.Translation == tangentOut.Translation) { return (SplineNode.SplinePointTangentNode)tangentNodes[i]; } } return null; } private static void SetSelectSplinePointNode(Spline spline, int index) { Editor.Instance.SceneEditing.Select(GetSplinePointNode(spline, index)); } private static void SetSelectTangentIn(Spline spline, int index) { Editor.Instance.SceneEditing.Select(GetSplineTangentInNode(spline, index)); } private static void SetSelectTangentOut(Spline spline, int index) { Editor.Instance.SceneEditing.Select(GetSplineTangentOutNode(spline, index)); } } }