// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEngine; using FlaxEditor.Actions; using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph.Actors; using FlaxEditor.GUI.Tabs; namespace FlaxEditor.CustomEditors.Dedicated { /// /// Custom editor for . /// /// [CustomEditor(typeof(Spline)), DefaultEditor] public class SplineEditor : ActorEditor { /// /// Storage undo spline data /// private struct UndoData { public Spline Spline; public BezierCurve.Keyframe[] BeforeKeyframes; } /// /// Basis for creating tangent manipulation types for bezier curves. /// private class EditTangentOptionBase { /// /// Called when user set selected tangent mode. /// /// Current spline selected on editor viewport. /// Index of current keyframe selected on spline. public virtual 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 virtual 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 virtual 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 virtual 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 virtual 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) || IsSmoothInTangentMode(spline, index) || IsSmoothOutTangentMode(spline, index)) { SetPointSmooth(spline, 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); } } /// /// 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 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 alignWithIn) { var keyframe = spline.GetSplineKeyframe(index); var referenceTangent = alignWithIn ? keyframe.TangentIn : keyframe.TangentOut; var otherTangent = !alignWithIn ? keyframe.TangentIn : keyframe.TangentOut; // inverse of reference tangent otherTangent.Translation = -referenceTangent.Translation.Normalized * otherTangent.Translation.Length; if (alignWithIn) keyframe.TangentOut = otherTangent; if (!alignWithIn) 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); } } /// /// 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); } } private sealed class IconTab : Tab { private sealed class IconTabHeader : Tabs.TabHeader { public IconTabHeader(Tabs tabs, Tab tab) : base(tabs, tab) { } public override bool OnMouseUp(Float2 location, MouseButton button) { if (EnabledInHierarchy && Tab.Enabled) ((IconTab)Tab)._action(); return true; } public override void Draw() { base.Draw(); var tab = (IconTab)Tab; var enabled = EnabledInHierarchy && tab.EnabledInHierarchy; var style = FlaxEngine.GUI.Style.Current; var size = Size; var textHeight = 16.0f; var iconSize = size.Y - textHeight; var iconRect = new Rectangle((Width - iconSize) / 2, 0, iconSize, iconSize); if (tab._mirrorIcon) { iconRect.Location.X += iconRect.Size.X; iconRect.Size.X *= -1; } var color = style.Foreground; if (!enabled) color *= 0.6f; Render2D.DrawSprite(tab._customIcon, iconRect, color); Render2D.DrawText(style.FontMedium, tab._customText, new Rectangle(0, iconSize, size.X, textHeight), color, TextAlignment.Center, TextAlignment.Center); } } private readonly Action _action; private readonly string _customText; private readonly SpriteHandle _customIcon; private readonly bool _mirrorIcon; public IconTab(Action action, string text, SpriteHandle icon, bool mirrorIcon = false) : base(string.Empty, SpriteHandle.Invalid) { _action = action; _customText = text; _customIcon = icon; _mirrorIcon = mirrorIcon; } public override Tabs.TabHeader CreateHeader() { return new IconTabHeader((Tabs)Parent, this); } } private EditTangentOptionBase _currentTangentMode; private Tabs _selectedPointsTabs, _allPointsTabs; private Tab _freeTangentTab; private Tab _linearTangentTab; private Tab _alignedTangentTab; private Tab _smoothInTangentTab; private Tab _smoothOutTangentTab; private Tab _setLinearAllTangentsTab; private Tab _setSmoothAllTangentsTab; private bool _tanInChanged; private bool _tanOutChanged; private Vector3 _lastTanInPos; private Vector3 _lastTanOutPos; private Spline _selectedSpline; private SplineNode.SplinePointNode _selectedPoint; private SplineNode.SplinePointNode _lastPointSelected; private SplineNode.SplinePointTangentNode _selectedTangentIn; private SplineNode.SplinePointTangentNode _selectedTangentOut; private UndoData[] _selectedSplinesUndoData; private bool HasPointSelected => _selectedPoint != null; private bool HasTangentsSelected => _selectedTangentIn != null || _selectedTangentOut != null; /// public override void Initialize(LayoutElementsContainer layout) { base.Initialize(layout); _currentTangentMode = new FreeTangentMode(); if (Values.HasDifferentTypes || !(Values[0] is Spline spline)) return; _selectedSpline = spline; layout.Space(10); var tabSize = 46; var icons = Editor.Instance.Icons; layout.Header("Selected spline point"); _selectedPointsTabs = new Tabs { Height = tabSize, TabsSize = new Float2(tabSize), AutoTabsSize = true, Parent = layout.ContainerControl, }; _linearTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedLinear, "Linear", icons.SplineLinear64)); _freeTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedFree, "Free", icons.SplineFree64)); _alignedTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedAligned, "Aligned", icons.SplineAligned64)); _smoothInTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedSmoothIn, "Smooth In", icons.SplineSmoothIn64)); _smoothOutTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedSmoothOut, "Smooth Out", icons.SplineSmoothIn64, true)); _selectedPointsTabs.SelectedTabIndex = -1; layout.Header("All spline points"); _allPointsTabs = new Tabs { Height = tabSize, TabsSize = new Float2(tabSize), AutoTabsSize = true, Parent = layout.ContainerControl, }; _setLinearAllTangentsTab = _allPointsTabs.AddTab(new IconTab(OnSetTangentsLinear, "Set Linear Tangents", icons.SplineLinear64)); _setSmoothAllTangentsTab = _allPointsTabs.AddTab(new IconTab(OnSetTangentsSmooth, "Set Smooth Tangents", icons.SplineAligned64)); _allPointsTabs.SelectedTabIndex = -1; if (_selectedSpline) _selectedSpline.SplineUpdated += OnSplineEdited; SetSelectedTangentTypeAsCurrent(); UpdateEditTabsSelection(); UpdateButtonsEnabled(); } /// protected override void Deinitialize() { if (_selectedSpline) _selectedSpline.SplineUpdated -= OnSplineEdited; } private void OnSplineEdited() { UpdateEditTabsSelection(); UpdateButtonsEnabled(); } /// public override void Refresh() { base.Refresh(); UpdateSelectedPoint(); UpdateSelectedTangent(); if (!CanEditTangent()) 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 void SetSelectedTangentTypeAsCurrent() { if (_lastPointSelected == null || _selectedPoint == null) return; if (IsLinearTangentMode(_selectedSpline, _lastPointSelected.Index)) SetModeLinear(); else if (IsAlignedTangentMode(_selectedSpline, _lastPointSelected.Index)) SetModeAligned(); else if (IsSmoothInTangentMode(_selectedSpline, _lastPointSelected.Index)) SetModeSmoothIn(); else if (IsSmoothOutTangentMode(_selectedSpline, _lastPointSelected.Index)) SetModeSmoothOut(); else if (IsFreeTangentMode(_selectedSpline, _lastPointSelected.Index)) SetModeFree(); } private void UpdateEditTabsSelection() { _selectedPointsTabs.Enabled = CanEditTangent(); if (!_selectedPointsTabs.Enabled) { _selectedPointsTabs.SelectedTabIndex = -1; 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; if (isFree) _selectedPointsTabs.SelectedTab = _freeTangentTab; else if (isLinear) _selectedPointsTabs.SelectedTab = _linearTangentTab; else if (isAligned) _selectedPointsTabs.SelectedTab = _alignedTangentTab; else if (isSmoothIn) _selectedPointsTabs.SelectedTab = _smoothInTangentTab; else if (isSmoothOut) _selectedPointsTabs.SelectedTab = _smoothOutTangentTab; else _selectedPointsTabs.SelectedTabIndex = -1; } private void UpdateButtonsEnabled() { _linearTangentTab.Enabled = CanEditTangent(); _freeTangentTab.Enabled = CanEditTangent(); _alignedTangentTab.Enabled = CanEditTangent(); _smoothInTangentTab.Enabled = CanSetTangentSmoothIn(); _smoothOutTangentTab.Enabled = CanSetTangentSmoothOut(); _setLinearAllTangentsTab.Enabled = CanSetAllTangentsLinear(); _setSmoothAllTangentsTab.Enabled = CanSetAllTangentsSmooth(); } private bool CanEditTangent() { return !HasDifferentTypes && !HasDifferentValues && (HasPointSelected || HasTangentsSelected); } private bool CanSetTangentSmoothIn() { if (!CanEditTangent()) return false; return _lastPointSelected.Index != 0; } private bool CanSetTangentSmoothOut() { if (!CanEditTangent()) return false; return _lastPointSelected.Index < _selectedSpline.SplinePointsCount - 1; } private bool CanSetAllTangentsSmooth() { return _selectedSpline != null; } private bool CanSetAllTangentsLinear() { return _selectedSpline != null; } private void SetModeLinear() { if (_currentTangentMode is LinearTangentMode) return; _currentTangentMode = new LinearTangentMode(); _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index); } private void SetModeFree() { if (_currentTangentMode is FreeTangentMode) return; _currentTangentMode = new FreeTangentMode(); _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index); } private void SetModeAligned() { if (_currentTangentMode is AlignedTangentMode) return; _currentTangentMode = new AlignedTangentMode(); _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index); } private void SetModeSmoothIn() { if (_currentTangentMode is SmoothInTangentMode) return; _currentTangentMode = new SmoothInTangentMode(); _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index); } private void SetModeSmoothOut() { if (_currentTangentMode is SmoothOutTangentMode) return; _currentTangentMode = new SmoothOutTangentMode(); _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index); } private void UpdateSelectedPoint() { // works only if select one spline if (_selectedSpline) { var currentSelected = Editor.Instance.SceneEditing.Selection[0]; if (currentSelected == _selectedPoint) return; if (currentSelected is SplineNode.SplinePointNode selectedPoint) { _selectedPoint = selectedPoint; _lastPointSelected = _selectedPoint; _currentTangentMode.OnSelectKeyframe(_selectedSpline, _lastPointSelected.Index); } else { _selectedPoint = null; } } else { _selectedPoint = null; } SetSelectedTangentTypeAsCurrent(); UpdateEditTabsSelection(); UpdateButtonsEnabled(); } 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 selectedPoint) { _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 = selectedPoint; _selectedTangentOut = null; _currentTangentMode.OnSelectTangent(_selectedSpline, index); return; } if (currentSelected.Transform == _selectedSpline.GetSplineTangent(index, false)) { _selectedTangentOut = selectedPoint; _selectedTangentIn = null; _currentTangentMode.OnSelectTangent(_selectedSpline, index); return; } _selectedTangentIn = null; _selectedTangentOut = null; } private void StartEditSpline() { if (Presenter.Undo != null && Presenter.Undo.Enabled) { // Capture 'before' state for undo var splines = new List(); for (int i = 0; i < Values.Count; i++) { if (Values[i] is Spline spline) { splines.Add(new UndoData { Spline = spline, BeforeKeyframes = spline.SplineKeyframes.Clone() as BezierCurve.Keyframe[] }); } } _selectedSplinesUndoData = splines.ToArray(); } } private void EndEditSpline() { // Update buttons state UpdateEditTabsSelection(); if (Presenter.Undo != null && Presenter.Undo.Enabled) { // Add undo foreach (var splineUndoData in _selectedSplinesUndoData) { Presenter.Undo.AddAction(new EditSplineAction(_selectedSpline, splineUndoData.BeforeKeyframes)); SplineNode.OnSplineEdited(splineUndoData.Spline); Editor.Instance.Scene.MarkSceneEdited(splineUndoData.Spline.Scene); } } } private void OnSetSelectedLinear() { StartEditSpline(); SetModeLinear(); EndEditSpline(); } private void OnSetSelectedFree() { StartEditSpline(); SetModeFree(); EndEditSpline(); } private void OnSetSelectedAligned() { StartEditSpline(); SetModeAligned(); EndEditSpline(); } private void OnSetSelectedSmoothIn() { StartEditSpline(); SetModeSmoothIn(); EndEditSpline(); } private void OnSetSelectedSmoothOut() { StartEditSpline(); SetModeSmoothOut(); EndEditSpline(); } private void OnSetTangentsLinear() { StartEditSpline(); _selectedSpline.SetTangentsLinear(); _selectedSpline.UpdateSpline(); EndEditSpline(); } private void OnSetTangentsSmooth() { StartEditSpline(); _selectedSpline.SetTangentsSmooth(); _selectedSpline.UpdateSpline(); EndEditSpline(); } 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); spline.UpdateSpline(); } 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 smoothRange = SplineNode.NodeSizeByDistance(spline.GetSplineTangent(index, false).Translation, 10f); var previousKeyframe = spline.GetSplineKeyframe(index - 1); var tangentDirection = keyframe.Value.WorldToLocalVector(previousKeyframe.Value.Translation - keyframe.Value.Translation); tangentDirection = tangentDirection.Normalized * smoothRange; keyframe.TangentIn.Translation = tangentDirection; } keyframe.TangentOut.Translation = Vector3.Zero; spline.SetSplineKeyframe(index, keyframe); spline.UpdateSpline(); } 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 smoothRange = SplineNode.NodeSizeByDistance(spline.GetSplineTangent(index, false).Translation, 10f); var nextKeyframe = spline.GetSplineKeyframe(index + 1); var tangentDirection = keyframe.Value.WorldToLocalVector(nextKeyframe.Value.Translation - keyframe.Value.Translation); tangentDirection = tangentDirection.Normalized * smoothRange; keyframe.TangentOut.Translation = tangentDirection; } keyframe.TangentIn.Translation = Vector3.Zero; spline.SetSplineKeyframe(index, keyframe); spline.UpdateSpline(); } private static void SetPointSmooth(Spline spline, int index) { var keyframe = spline.GetSplineKeyframe(index); var tangentInSize = keyframe.TangentIn.Translation.Length; var tangentOutSize = keyframe.TangentOut.Translation.Length; var isLastKeyframe = index >= spline.SplinePointsCount - 1; var isFirstKeyframe = index <= 0; var smoothRange = SplineNode.NodeSizeByDistance(spline.GetSplinePoint(index), 10f); // Force smooth it's linear point if (tangentInSize == 0f) tangentInSize = smoothRange; if (tangentOutSize == 0f) tangentOutSize = smoothRange; // Try get next / last keyframe var nextKeyframe = !isLastKeyframe ? spline.GetSplineKeyframe(index + 1) : keyframe; var previousKeyframe = !isFirstKeyframe ? spline.GetSplineKeyframe(index - 1) : keyframe; // calc form from Spline.cpp -> SetTangentsSmooth // get tangent direction var tangentDirection = (keyframe.Value.Translation - previousKeyframe.Value.Translation + nextKeyframe.Value.Translation - keyframe.Value.Translation).Normalized; keyframe.TangentIn.Translation = -tangentDirection; keyframe.TangentOut.Translation = tangentDirection; keyframe.TangentIn.Translation *= tangentInSize; keyframe.TangentOut.Translation *= tangentOutSize; spline.SetSplineKeyframe(index, keyframe); spline.UpdateSpline(); } 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)); } } }