// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
namespace FlaxEditor.GUI.Timeline.Tracks
{
///
/// The timeline track for animating object property via Curve.
///
///
public abstract class CurvePropertyTrackBase : MemberTrack, IKeyframesEditorContext
{
private sealed class Splitter : Control
{
private bool _clicked;
internal CurvePropertyTrackBase _track;
public override void Draw()
{
var style = Style.Current;
if (IsMouseOver || _clicked)
Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), _clicked ? style.BackgroundSelected : style.BackgroundHighlighted);
}
public override void OnEndMouseCapture()
{
base.OnEndMouseCapture();
_clicked = false;
}
public override void Defocus()
{
base.Defocus();
_clicked = false;
}
public override void OnMouseEnter(Float2 location)
{
base.OnMouseEnter(location);
Cursor = CursorType.SizeNS;
}
public override void OnMouseLeave()
{
Cursor = CursorType.Default;
base.OnMouseLeave();
}
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_clicked = true;
Focus();
StartMouseCapture();
return true;
}
return base.OnMouseDown(location, button);
}
public override void OnMouseMove(Float2 location)
{
base.OnMouseMove(location);
if (_clicked)
{
var height = Mathf.Clamp(PointToParent(location).Y, 40.0f, 1000.0f);
if (!Mathf.NearEqual(height, _track._expandedHeight))
{
_track.Height = _track._expandedHeight = height;
_track.Timeline.ArrangeTracks();
}
}
}
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && _clicked)
{
_clicked = false;
EndMouseCapture();
return true;
}
return base.OnMouseUp(location, button);
}
}
private byte[] _curveEditingStartData;
private float _expandedHeight = 120.0f;
private Splitter _splitter;
///
/// The curve editor.
///
public CurveEditorBase Curve;
private const float CollapsedHeight = 20.0f;
///
public CurvePropertyTrackBase(ref TrackCreateOptions options)
: base(ref options)
{
Height = CollapsedHeight;
_addKey.Clicked += OnAddKeyClicked;
_leftKey.Clicked += OnLeftKeyClicked;
_rightKey.Clicked += OnRightKeyClicked;
}
private void OnRightKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && GetNextKeyframeFrame(Timeline.CurrentTime, out var frame))
{
Timeline.OnSeek(frame);
}
}
///
protected override void OnContextMenu(ContextMenu.ContextMenu menu)
{
base.OnContextMenu(menu);
if (Curve == null)
return;
menu.AddSeparator();
menu.AddButton("Copy Preview Value", () =>
{
Curve.Evaluate(out var value, Timeline.CurrentTime);
Clipboard.Text = FlaxEngine.Json.JsonSerializer.Serialize(value);
}).LinkTooltip("Copies the current track value to the clipboard").Enabled = Timeline.ShowPreviewValues;
}
///
public override bool GetNextKeyframeFrame(float time, out int result)
{
if (Curve != null)
{
for (int i = 0; i < Curve.KeyframesCount; i++)
{
Curve.GetKeyframe(i, out var kTime, out _, out _, out _);
if (kTime > time)
{
result = Mathf.FloorToInt(kTime * Timeline.FramesPerSecond);
return true;
}
}
}
return base.GetNextKeyframeFrame(time, out result);
}
private void OnAddKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && Curve != null)
{
// Evaluate a value
var time = Timeline.CurrentTime;
if (!TryGetValue(out var value))
Curve.Evaluate(out value, time);
// Find keyframe at the current location
for (int i = Curve.KeyframesCount - 1; i >= 0; i--)
{
Curve.GetKeyframe(i, out var kTime, out var kValue, out _, out _);
var frame = Mathf.FloorToInt(kTime * Timeline.FramesPerSecond);
if (frame == Timeline.CurrentFrame)
{
// Skip if value is the same
if (Equals(kValue, value))
return;
// Update existing key value
Curve.SetKeyframeValue(i, value);
UpdatePreviewValue();
return;
}
}
// Add a new key
using (new TrackUndoBlock(this))
Curve.AddKeyframe(time, value);
}
}
private void OnLeftKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && GetPreviousKeyframeFrame(Timeline.CurrentTime, out var frame))
{
Timeline.OnSeek(frame);
}
}
///
public override bool GetPreviousKeyframeFrame(float time, out int result)
{
if (Curve != null)
{
for (int i = Curve.KeyframesCount - 1; i >= 0; i--)
{
Curve.GetKeyframe(i, out var kTime, out _, out _, out _);
if (kTime < time)
{
result = Mathf.FloorToInt(kTime * Timeline.FramesPerSecond);
return true;
}
}
}
return base.GetPreviousKeyframeFrame(time, out result);
}
private void UpdateCurve()
{
if (Curve == null || Timeline == null)
return;
bool wasVisible = Curve.Visible;
Curve.Visible = Visible;
if (!Visible)
{
if (wasVisible)
Curve.ClearSelection();
return;
}
var expanded = IsExpanded;
Curve.KeyframesEditorContext = Timeline;
Curve.CustomViewPanning = Timeline.OnKeyframesViewPanning;
Curve.Bounds = new Rectangle(Timeline.StartOffset, Y + 1.0f, Timeline.Duration * Timeline.UnitsPerSecond * Timeline.Zoom, Height - 2.0f);
Curve.ViewScale = new Float2(Timeline.Zoom, Curve.ViewScale.Y);
Curve.ShowCollapsed = !expanded;
Curve.ShowAxes = expanded ? CurveEditorBase.UseMode.Horizontal : CurveEditorBase.UseMode.Off;
Curve.EnableZoom = expanded ? CurveEditorBase.UseMode.Vertical : CurveEditorBase.UseMode.Off;
Curve.EnablePanning = expanded ? CurveEditorBase.UseMode.Vertical : CurveEditorBase.UseMode.Off;
Curve.ScrollBars = expanded ? ScrollBars.Vertical : ScrollBars.None;
Curve.UpdateKeyframes();
if (expanded)
{
if (_splitter == null)
{
_splitter = new Splitter
{
_track = this,
Parent = Curve,
};
}
var splitterHeight = 5.0f;
_splitter.Bounds = new Rectangle(0, Curve.Height - splitterHeight, Curve.Width, splitterHeight);
}
}
private void OnKeyframesEdited()
{
UpdatePreviewValue();
Timeline.MarkAsEdited();
}
private void OnCurveEditingStart()
{
_curveEditingStartData = EditTrackAction.CaptureData(this);
}
private void OnCurveEditingEnd()
{
var after = EditTrackAction.CaptureData(this);
if (!Utils.ArraysEqual(_curveEditingStartData, after))
Timeline.AddBatchedUndoAction(new EditTrackAction(Timeline, this, _curveEditingStartData, after));
_curveEditingStartData = null;
}
private void UpdatePreviewValue()
{
if (Curve == null)
{
_previewValue.Text = string.Empty;
return;
}
var time = Timeline.CurrentTime;
Curve.Evaluate(out var value, time);
_previewValue.Text = GetValueText(value);
}
///
/// Creates the curve.
///
/// Type of the property (keyframes value).
/// Type of the curve editor (generic type of the curve editor).
protected void CreateCurve(Type propertyType, Type curveEditorType)
{
curveEditorType = curveEditorType.MakeGenericType(propertyType);
Curve = (CurveEditorBase)Activator.CreateInstance(curveEditorType);
Curve.EnableZoom = CurveEditorBase.UseMode.Vertical;
Curve.EnablePanning = CurveEditorBase.UseMode.Vertical;
Curve.ShowBackground = false;
Curve.ScrollBars = ScrollBars.Vertical;
Curve.Parent = Timeline?.MediaPanel;
Curve.FPS = Timeline?.FramesPerSecond;
Curve.Edited += OnKeyframesEdited;
Curve.EditingStart += OnCurveEditingStart;
Curve.EditingEnd += OnCurveEditingEnd;
if (Timeline != null)
{
Curve.UnlockChildrenRecursive();
UpdateCurve();
}
}
private void DisposeCurve()
{
if (Curve == null)
return;
Curve.Edited -= OnKeyframesEdited;
Curve.Dispose();
Curve = null;
_splitter = null;
}
///
public override object Evaluate(float time)
{
if (Curve != null)
{
Curve.Evaluate(out var result, time);
return result;
}
return base.Evaluate(time);
}
///
public override bool CanExpand => true;
///
protected override void OnMemberChanged(MemberInfo value, Type type)
{
base.OnMemberChanged(value, type);
DisposeCurve();
}
///
protected override void OnVisibleChanged()
{
base.OnVisibleChanged();
UpdateCurve();
}
///
protected override void OnExpandedChanged()
{
Height = IsExpanded ? _expandedHeight : CollapsedHeight;
UpdateCurve();
if (IsExpanded)
Curve.ShowWholeCurve();
base.OnExpandedChanged();
}
///
public override void OnTimelineChanged(Timeline timeline)
{
base.OnTimelineChanged(timeline);
if (Curve != null)
{
Curve.Parent = timeline?.MediaPanel;
Curve.FPS = timeline?.FramesPerSecond;
UpdateCurve();
}
UpdatePreviewValue();
}
///
public override void OnUndo()
{
base.OnUndo();
UpdatePreviewValue();
}
///
public override void OnTimelineZoomChanged()
{
base.OnTimelineZoomChanged();
UpdateCurve();
}
///
public override void OnTimelineArrange()
{
base.OnTimelineArrange();
UpdateCurve();
}
///
public override void OnTimelineFpsChanged(float before, float after)
{
base.OnTimelineFpsChanged(before, after);
if (Curve != null)
{
Curve.FPS = after;
}
UpdatePreviewValue();
}
///
public override void OnTimelineCurrentFrameChanged(int frame)
{
base.OnTimelineCurrentFrameChanged(frame);
UpdatePreviewValue();
}
///
public override void OnDestroy()
{
DisposeCurve();
base.OnDestroy();
}
///
public new void OnKeyframesDeselect(IKeyframesEditor editor)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesDeselect(editor);
}
///
public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesSelection(editor, control, selection);
}
///
public new int OnKeyframesSelectionCount()
{
return Curve != null && Curve.Visible ? Curve.OnKeyframesSelectionCount() : 0;
}
///
public new void OnKeyframesDelete(IKeyframesEditor editor)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesDelete(editor);
}
///
public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesMove(editor, control, location, start, end);
}
///
public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesCopy(editor, timeOffset, data);
}
///
public new void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
///
public new void OnKeyframesGet(Action get)
{
Curve?.OnKeyframesGet(Name, get);
}
///
public new void OnKeyframesSet(List> keyframes)
{
Curve?.OnKeyframesSet(keyframes);
}
}
///
/// The timeline track for animating object property via Bezier Curve.
///
///
///
public sealed class CurvePropertyTrack : CurvePropertyTrackBase
{
///
/// Gets the archetype.
///
/// The archetype.
public static TrackArchetype GetArchetype()
{
return new TrackArchetype
{
TypeId = 10,
Name = "Property",
DisableSpawnViaGUI = true,
Create = options => new CurvePropertyTrack(ref options),
Load = LoadTrack,
Save = SaveTrack,
};
}
private static void LoadTrack(int version, Track track, BinaryReader stream)
{
var e = (CurvePropertyTrack)track;
int valueSize = stream.ReadInt32();
int propertyNameLength = stream.ReadInt32();
int propertyTypeNameLength = stream.ReadInt32();
int keyframesCount = stream.ReadInt32();
var propertyName = stream.ReadBytes(propertyNameLength);
e.MemberName = Encoding.UTF8.GetString(propertyName, 0, propertyNameLength);
if (stream.ReadChar() != 0)
throw new Exception("Invalid track data.");
var propertyTypeName = stream.ReadBytes(propertyTypeNameLength);
e.MemberTypeName = Encoding.UTF8.GetString(propertyTypeName, 0, propertyTypeNameLength);
if (stream.ReadChar() != 0)
throw new Exception("Invalid track data.");
var keyframes = new object[keyframesCount];
var propertyType = TypeUtils.GetType(e.MemberTypeName).Type;
if (propertyType == null)
{
stream.ReadBytes(keyframesCount * (sizeof(float) + valueSize * 3));
if (!string.IsNullOrEmpty(e.MemberTypeName))
Editor.LogError("Cannot load track " + e.MemberName + " of type " + e.MemberTypeName + ". Failed to find the value type information.");
return;
}
e.ValueSize = e.GetValueDataSize(propertyType);
var dataBuffer = new byte[valueSize];
GCHandle handle = GCHandle.Alloc(dataBuffer, GCHandleType.Pinned);
for (int i = 0; i < keyframesCount; i++)
{
var time = stream.ReadSingle();
var value = ReadValue(stream, ref handle, dataBuffer, e.ValueSize, propertyType);
var tangentIn = ReadValue(stream, ref handle, dataBuffer, e.ValueSize, propertyType);
var tangentOut = ReadValue(stream, ref handle, dataBuffer, e.ValueSize, propertyType);
keyframes[i] = new BezierCurve