1494 lines
50 KiB
C#
1494 lines
50 KiB
C#
// Copyright (c) Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using FlaxEditor.CustomEditors;
|
|
using FlaxEditor.GUI.ContextMenu;
|
|
using FlaxEditor.Options;
|
|
using FlaxEngine;
|
|
using FlaxEngine.GUI;
|
|
using FlaxEngine.Json;
|
|
using FlaxEngine.Utilities;
|
|
|
|
namespace FlaxEditor.GUI
|
|
{
|
|
/// <summary>
|
|
/// The generic keyframes animation editor control.
|
|
/// </summary>
|
|
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
|
[HideInEditor]
|
|
public class KeyframesEditor : ContainerControl, IKeyframesEditor
|
|
{
|
|
/// <summary>
|
|
/// A single keyframe.
|
|
/// </summary>
|
|
public struct Keyframe : IComparable, IComparable<Keyframe>
|
|
{
|
|
/// <summary>
|
|
/// The time of the keyframe.
|
|
/// </summary>
|
|
[EditorOrder(0), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("The time of the keyframe.")]
|
|
public float Time;
|
|
|
|
/// <summary>
|
|
/// The value of the keyframe.
|
|
/// </summary>
|
|
[EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("The value of the keyframe.")]
|
|
public object Value;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Keyframe"/> struct.
|
|
/// </summary>
|
|
/// <param name="time">The time.</param>
|
|
/// <param name="value">The value.</param>
|
|
public Keyframe(float time, object value)
|
|
{
|
|
Time = time;
|
|
Value = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int CompareTo(object obj)
|
|
{
|
|
if (obj is Keyframe other)
|
|
return Time > other.Time ? 1 : 0;
|
|
return 1;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int CompareTo(Keyframe other)
|
|
{
|
|
return Time > other.Time ? 1 : 0;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
return Value?.ToString() ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The keyframes contents container control.
|
|
/// </summary>
|
|
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
|
private class Contents : ContainerControl
|
|
{
|
|
private readonly KeyframesEditor _editor;
|
|
internal bool _leftMouseDown;
|
|
private bool _rightMouseDown;
|
|
internal Float2 _leftMouseDownPos = Float2.Minimum;
|
|
private Float2 _rightMouseDownPos = Float2.Minimum;
|
|
internal Float2 _mousePos = Float2.Minimum;
|
|
private Float2 _movingViewLastPos;
|
|
internal bool _isMovingSelection;
|
|
private bool _movedKeyframes;
|
|
private float _movingSelectionStart;
|
|
private float[] _movingSelectionOffsets;
|
|
private Float2 _cmShowPos;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Contents"/> class.
|
|
/// </summary>
|
|
/// <param name="editor">The editor.</param>
|
|
public Contents(KeyframesEditor editor)
|
|
{
|
|
_editor = editor;
|
|
}
|
|
|
|
private void UpdateSelectionRectangle()
|
|
{
|
|
var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos);
|
|
if (_editor.KeyframesEditorContext != null)
|
|
_editor.KeyframesEditorContext.OnKeyframesSelection(_editor, this, selectionRect);
|
|
else
|
|
UpdateSelection(ref selectionRect);
|
|
}
|
|
|
|
internal void UpdateSelection(ref Rectangle selectionRect)
|
|
{
|
|
// Find controls to select
|
|
for (int i = 0; i < Children.Count; i++)
|
|
{
|
|
if (Children[i] is KeyframePoint p)
|
|
{
|
|
p.IsSelected = p.Bounds.Intersects(selectionRect);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void OnMoveStart(Float2 location)
|
|
{
|
|
// Start moving selected nodes
|
|
_isMovingSelection = true;
|
|
_movedKeyframes = false;
|
|
var viewRect = _editor._mainPanel.GetClientArea();
|
|
_movingSelectionStart = PointToKeyframes(location, ref viewRect).X;
|
|
if (_movingSelectionOffsets == null || _movingSelectionOffsets.Length != _editor._keyframes.Count)
|
|
_movingSelectionOffsets = new float[_editor._keyframes.Count];
|
|
for (int i = 0; i < _movingSelectionOffsets.Length; i++)
|
|
_movingSelectionOffsets[i] = _editor._keyframes[i].Time - _movingSelectionStart;
|
|
_editor.OnEditingStart();
|
|
}
|
|
|
|
internal void OnMove(Float2 location)
|
|
{
|
|
var viewRect = _editor._mainPanel.GetClientArea();
|
|
var locationKeyframes = PointToKeyframes(location, ref viewRect);
|
|
for (var i = 0; i < _editor._points.Count; i++)
|
|
{
|
|
var p = _editor._points[i];
|
|
if (p.IsSelected)
|
|
{
|
|
var k = _editor._keyframes[p.Index];
|
|
|
|
float minTime = p.Index != 0 ? _editor._keyframes[p.Index - 1].Time : float.MinValue;
|
|
float maxTime = p.Index != _editor._keyframes.Count - 1 ? _editor._keyframes[p.Index + 1].Time : float.MaxValue;
|
|
|
|
var offset = _movingSelectionOffsets[p.Index];
|
|
k.Time = locationKeyframes.X + offset;
|
|
if (_editor.FPS.HasValue)
|
|
{
|
|
float fps = _editor.FPS.Value;
|
|
k.Time = Mathf.Floor(k.Time * fps) / fps;
|
|
}
|
|
k.Time = Mathf.Clamp(k.Time, minTime, maxTime);
|
|
|
|
// TODO: snapping keyframes to grid when moving
|
|
|
|
_editor._keyframes[p.Index] = k;
|
|
}
|
|
_editor.UpdateKeyframes();
|
|
if (_editor.EnablePanning)
|
|
{
|
|
//_editor._mainPanel.ScrollViewTo(PointToParent(_editor._mainPanel, location));
|
|
}
|
|
Cursor = CursorType.SizeAll;
|
|
_movedKeyframes = true;
|
|
}
|
|
}
|
|
|
|
internal void OnMoveEnd(Float2 location)
|
|
{
|
|
if (_movedKeyframes)
|
|
{
|
|
_editor.OnEdited();
|
|
_editor.OnEditingEnd();
|
|
_editor.UpdateKeyframes();
|
|
_movedKeyframes = false;
|
|
}
|
|
_isMovingSelection = false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool IntersectsContent(ref Float2 locationParent, out Float2 location)
|
|
{
|
|
// Pass all events
|
|
location = PointFromParent(ref locationParent);
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnMouseEnter(Float2 location)
|
|
{
|
|
_mousePos = location;
|
|
|
|
base.OnMouseEnter(location);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnMouseMove(Float2 location)
|
|
{
|
|
_mousePos = location;
|
|
|
|
// Start moving selection if movement started from the keyframe
|
|
if (_leftMouseDown && !_isMovingSelection && GetChildAt(_leftMouseDownPos) is KeyframePoint)
|
|
{
|
|
if (_editor.KeyframesEditorContext != null)
|
|
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, true, false);
|
|
else
|
|
OnMoveStart(location);
|
|
}
|
|
|
|
// Moving view
|
|
if (_rightMouseDown)
|
|
{
|
|
// Calculate delta
|
|
Float2 delta = location - _movingViewLastPos;
|
|
if (delta.LengthSquared > 0.01f)
|
|
{
|
|
if (_editor.CustomViewPanning != null)
|
|
delta = _editor.CustomViewPanning(delta);
|
|
if (_editor.EnablePanning)
|
|
{
|
|
// Move view
|
|
_editor.ViewOffset += delta * _editor.ViewScale;
|
|
_movingViewLastPos = location;
|
|
Cursor = CursorType.SizeAll;
|
|
}
|
|
else if (_editor.CustomViewPanning != null)
|
|
Cursor = CursorType.SizeAll;
|
|
}
|
|
|
|
return;
|
|
}
|
|
// Moving selection
|
|
else if (_isMovingSelection)
|
|
{
|
|
if (_editor.KeyframesEditorContext != null)
|
|
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, false, false);
|
|
else
|
|
OnMove(location);
|
|
return;
|
|
}
|
|
// Selecting
|
|
else if (_leftMouseDown)
|
|
{
|
|
UpdateSelectionRectangle();
|
|
return;
|
|
}
|
|
|
|
base.OnMouseMove(location);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLostFocus()
|
|
{
|
|
// Clear flags and state
|
|
if (_leftMouseDown)
|
|
{
|
|
_leftMouseDown = false;
|
|
}
|
|
if (_rightMouseDown)
|
|
{
|
|
_rightMouseDown = false;
|
|
Cursor = CursorType.Default;
|
|
}
|
|
_isMovingSelection = false;
|
|
|
|
base.OnLostFocus();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDown(Float2 location, MouseButton button)
|
|
{
|
|
if (base.OnMouseDown(location, button))
|
|
{
|
|
// Clear flags
|
|
_isMovingSelection = false;
|
|
_rightMouseDown = false;
|
|
_leftMouseDown = false;
|
|
return true;
|
|
}
|
|
|
|
// Cache data
|
|
_isMovingSelection = false;
|
|
_mousePos = location;
|
|
if (button == MouseButton.Left)
|
|
{
|
|
_leftMouseDown = true;
|
|
_leftMouseDownPos = location;
|
|
}
|
|
if (button == MouseButton.Right)
|
|
{
|
|
_rightMouseDown = true;
|
|
_rightMouseDownPos = location;
|
|
_movingViewLastPos = location;
|
|
}
|
|
|
|
// Check if any node is under the mouse
|
|
var underMouse = GetChildAt(location);
|
|
if (underMouse is KeyframePoint keyframe)
|
|
{
|
|
if (_leftMouseDown)
|
|
{
|
|
if (Root.GetKey(KeyboardKeys.Control))
|
|
{
|
|
// Toggle selection
|
|
keyframe.IsSelected = !keyframe.IsSelected;
|
|
}
|
|
else if (Root.GetKey(KeyboardKeys.Shift))
|
|
{
|
|
// Select range
|
|
keyframe.IsSelected = true;
|
|
int selectionStart = 0;
|
|
for (; selectionStart < _editor._points.Count; selectionStart++)
|
|
{
|
|
if (_editor._points[selectionStart].IsSelected)
|
|
break;
|
|
}
|
|
int selectionEnd = _editor._points.Count - 1;
|
|
for (; selectionEnd > selectionStart; selectionEnd--)
|
|
{
|
|
if (_editor._points[selectionEnd].IsSelected)
|
|
break;
|
|
}
|
|
selectionStart++;
|
|
for (; selectionStart < selectionEnd; selectionStart++)
|
|
_editor._points[selectionStart].IsSelected = true;
|
|
}
|
|
else if (!keyframe.IsSelected)
|
|
{
|
|
// Select node
|
|
if (_editor.KeyframesEditorContext != null)
|
|
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
|
else
|
|
_editor.ClearSelection();
|
|
keyframe.IsSelected = true;
|
|
}
|
|
StartMouseCapture();
|
|
Focus();
|
|
Tooltip?.Hide();
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_leftMouseDown)
|
|
{
|
|
// Start selecting
|
|
StartMouseCapture();
|
|
if (_editor.KeyframesEditorContext != null)
|
|
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
|
else
|
|
_editor.ClearSelection();
|
|
Focus();
|
|
return true;
|
|
}
|
|
if (_rightMouseDown)
|
|
{
|
|
// Start navigating
|
|
StartMouseCapture();
|
|
Focus();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Focus();
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseUp(Float2 location, MouseButton button)
|
|
{
|
|
_mousePos = location;
|
|
|
|
if (_leftMouseDown && button == MouseButton.Left)
|
|
{
|
|
_leftMouseDown = false;
|
|
EndMouseCapture();
|
|
Cursor = CursorType.Default;
|
|
|
|
// Moving keyframes
|
|
if (_isMovingSelection)
|
|
{
|
|
if (_editor.KeyframesEditorContext != null)
|
|
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, false, true);
|
|
else
|
|
OnMoveEnd(location);
|
|
}
|
|
|
|
_isMovingSelection = false;
|
|
_movedKeyframes = false;
|
|
}
|
|
if (_rightMouseDown && button == MouseButton.Right)
|
|
{
|
|
_rightMouseDown = false;
|
|
EndMouseCapture();
|
|
Cursor = CursorType.Default;
|
|
|
|
// Check if no move has been made at all
|
|
if (Float2.Distance(location, _rightMouseDownPos) < 2.0f)
|
|
{
|
|
var selectionCount = _editor.SelectionCount;
|
|
var point = GetChildAt(location) as KeyframePoint;
|
|
if (selectionCount == 0 && point != null)
|
|
{
|
|
// Select node
|
|
selectionCount = 1;
|
|
point.IsSelected = true;
|
|
}
|
|
|
|
var viewRect = _editor._mainPanel.GetClientArea();
|
|
_cmShowPos = PointToKeyframes(location, ref viewRect);
|
|
|
|
var cm = new ContextMenu.ContextMenu();
|
|
cm.AddButton("Add keyframe", () => _editor.AddKeyframe(_cmShowPos)).Enabled = _editor.Keyframes.Count < _editor.MaxKeyframes && _editor.DefaultValue != null;
|
|
if (selectionCount > 0 && _editor.EnableKeyframesValueEdit)
|
|
{
|
|
cm.AddButton(selectionCount == 1 ? "Edit keyframe" : "Edit keyframes", () => _editor.EditKeyframes(this, location));
|
|
}
|
|
var totalSelectionCount = _editor.KeyframesEditorContext?.OnKeyframesSelectionCount() ?? selectionCount;
|
|
if (totalSelectionCount > 0)
|
|
{
|
|
cm.AddButton(totalSelectionCount == 1 ? "Remove keyframe" : "Remove keyframes", _editor.RemoveKeyframes);
|
|
cm.AddButton(totalSelectionCount == 1 ? "Copy keyframe" : "Copy keyframes", () => _editor.CopyKeyframes(point));
|
|
}
|
|
cm.AddButton("Paste keyframes", () => KeyframesEditorUtils.Paste(_editor, point?.Time ?? _cmShowPos.X)).Enabled = KeyframesEditorUtils.CanPaste();
|
|
cm.AddSeparator();
|
|
if (_editor.EnableKeyframesValueEdit)
|
|
cm.AddButton("Edit all keyframes", () => _editor.EditAllKeyframes(this, location));
|
|
cm.AddButton("Select all keyframes", _editor.SelectAll).Enabled = _editor._points.Count > 0;
|
|
cm.AddButton("Deselect all keyframes", _editor.DeselectAll).Enabled = _editor._points.Count > 0;
|
|
cm.AddButton("Copy all keyframes", () =>
|
|
{
|
|
_editor.SelectAll();
|
|
_editor.CopyKeyframes(point);
|
|
}).Enabled = _editor.DefaultValue != null;
|
|
if (_editor.EnableZoom && _editor.EnablePanning)
|
|
{
|
|
cm.AddSeparator();
|
|
cm.AddButton("Show whole keyframes", _editor.ShowWholeKeyframes);
|
|
cm.AddButton("Reset view", _editor.ResetView);
|
|
}
|
|
cm.Show(this, location);
|
|
}
|
|
}
|
|
|
|
if (base.OnMouseUp(location, button))
|
|
{
|
|
// Clear flags
|
|
_rightMouseDown = false;
|
|
_leftMouseDown = false;
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseWheel(Float2 location, float delta)
|
|
{
|
|
if (base.OnMouseWheel(location, delta))
|
|
return true;
|
|
|
|
// Zoom in/out
|
|
if (_editor.EnableZoom && IsMouseOver && !_leftMouseDown)
|
|
{
|
|
// TODO: preserve the view center point for easier zooming
|
|
_editor.ViewScale += delta * 0.1f;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void SetScaleInternal(ref Float2 scale)
|
|
{
|
|
base.SetScaleInternal(ref scale);
|
|
|
|
_editor.UpdateKeyframes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the input point from editor contents control space into the keyframes time/value coordinates.
|
|
/// </summary>
|
|
/// <param name="point">The point.</param>
|
|
/// <param name="keyframesContentAreaBounds">The keyframes contents area bounds.</param>
|
|
/// <returns>The result.</returns>
|
|
private Float2 PointToKeyframes(Float2 point, ref Rectangle keyframesContentAreaBounds)
|
|
{
|
|
// Contents -> Keyframes
|
|
return new Float2(
|
|
(point.X + Location.X) / UnitsPerSecond,
|
|
(point.Y + Location.Y - keyframesContentAreaBounds.Height) / -UnitsPerSecond
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The single keyframe control.
|
|
/// </summary>
|
|
private class KeyframePoint : Control
|
|
{
|
|
/// <summary>
|
|
/// The parent keyframes editor.
|
|
/// </summary>
|
|
public KeyframesEditor Editor;
|
|
|
|
/// <summary>
|
|
/// The keyframe index.
|
|
/// </summary>
|
|
public int Index;
|
|
|
|
/// <summary>
|
|
/// Flag for selected keyframes.
|
|
/// </summary>
|
|
public bool IsSelected;
|
|
|
|
/// <summary>
|
|
/// Gets the time of the keyframe.
|
|
/// </summary>
|
|
public float Time => Editor._keyframes[Index].Time;
|
|
|
|
/// <inheritdoc />
|
|
public override void Draw()
|
|
{
|
|
var rect = new Rectangle(Float2.Zero, Size);
|
|
var color = Color.Gray;
|
|
if (IsSelected)
|
|
color = Editor.ContainsFocus ? Color.YellowGreen : Color.Lerp(Color.Gray, Color.YellowGreen, 0.4f);
|
|
if (IsMouseOver)
|
|
color *= 1.1f;
|
|
Render2D.FillRectangle(rect, color);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnLocationChanged()
|
|
{
|
|
base.OnLocationChanged();
|
|
|
|
UpdateTooltip();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
|
|
{
|
|
if (base.OnMouseDoubleClick(location, button))
|
|
return true;
|
|
|
|
if (button == MouseButton.Left)
|
|
{
|
|
Editor.EditKeyframes(this, location, new List<int> { Index });
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the tooltip.
|
|
/// </summary>
|
|
public void UpdateTooltip()
|
|
{
|
|
var k = Editor._keyframes[Index];
|
|
TooltipText = string.Format("Time: {0}, Value: {1}", k.Time, k.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The timeline units per second (on time axis).
|
|
/// </summary>
|
|
public static readonly float UnitsPerSecond = 100.0f;
|
|
|
|
/// <summary>
|
|
/// The keyframes size.
|
|
/// </summary>
|
|
private static readonly Float2 KeyframesSize = new Float2(7.0f);
|
|
|
|
private Contents _contents;
|
|
private Panel _mainPanel;
|
|
private readonly List<KeyframePoint> _points = new List<KeyframePoint>();
|
|
private bool _refreshAfterEdit;
|
|
private Popup _popup;
|
|
private float? _fps;
|
|
|
|
/// <summary>
|
|
/// The keyframes collection.
|
|
/// </summary>
|
|
protected readonly List<Keyframe> _keyframes = new List<Keyframe>();
|
|
|
|
/// <summary>
|
|
/// Occurs when keyframes collection gets changed (keyframe added or removed).
|
|
/// </summary>
|
|
public event Action KeyframesChanged;
|
|
|
|
/// <summary>
|
|
/// Gets the keyframes collection (read-only).
|
|
/// </summary>
|
|
public IReadOnlyList<Keyframe> Keyframes => _keyframes;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the view offset (via scroll bars).
|
|
/// </summary>
|
|
public Float2 ViewOffset
|
|
{
|
|
get => _mainPanel.ViewOffset;
|
|
set => _mainPanel.ViewOffset = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the view scale.
|
|
/// </summary>
|
|
public Float2 ViewScale
|
|
{
|
|
get => _contents.Scale;
|
|
set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(1000.0f));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when keyframes data gets edited.
|
|
/// </summary>
|
|
public event Action Edited;
|
|
|
|
/// <summary>
|
|
/// Occurs when keyframes data editing starts (via UI).
|
|
/// </summary>
|
|
public event Action EditingStart;
|
|
|
|
/// <summary>
|
|
/// Occurs when keyframes data editing ends (via UI).
|
|
/// </summary>
|
|
public event Action EditingEnd;
|
|
|
|
/// <summary>
|
|
/// The function for custom view panning. Gets input movement delta (in keyframes editor control space) and returns the renaming input delta to process by keyframes editor itself.
|
|
/// </summary>
|
|
public Func<Float2, Float2> CustomViewPanning;
|
|
|
|
/// <summary>
|
|
/// The maximum amount of keyframes to use.
|
|
/// </summary>
|
|
public int MaxKeyframes = ushort.MaxValue;
|
|
|
|
/// <summary>
|
|
/// True if enable view zooming. Otherwise user won't be able to zoom in or out.
|
|
/// </summary>
|
|
public bool EnableZoom = true;
|
|
|
|
/// <summary>
|
|
/// True if enable view panning. Otherwise user won't be able to move the view area.
|
|
/// </summary>
|
|
public bool EnablePanning = true;
|
|
|
|
/// <summary>
|
|
/// True if enable keyframes values editing (via popup).
|
|
/// </summary>
|
|
public bool EnableKeyframesValueEdit = true;
|
|
|
|
/// <summary>
|
|
/// True if enable view panning. Otherwise user won't be able to move the view area.
|
|
/// </summary>
|
|
public bool Enable = true;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether user is editing the keyframes.
|
|
/// </summary>
|
|
public bool IsUserEditing => _popup != null || _contents._leftMouseDown;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the scroll bars usage.
|
|
/// </summary>
|
|
public ScrollBars ScrollBars
|
|
{
|
|
get => _mainPanel.ScrollBars;
|
|
set => _mainPanel.ScrollBars = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default value.
|
|
/// </summary>
|
|
public object DefaultValue;
|
|
|
|
/// <summary>
|
|
/// The amount of frames per second of the keyframes animation (optional). Can be used to restrict the keyframes time values to the given time quantization rate.
|
|
/// </summary>
|
|
public float? FPS
|
|
{
|
|
get => _fps;
|
|
set
|
|
{
|
|
if (_fps.HasValue == value.HasValue && (!value.HasValue || Mathf.NearEqual(_fps.Value, value.Value)))
|
|
return;
|
|
|
|
_fps = value;
|
|
UpdateFPS();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="KeyframesEditor"/> class.
|
|
/// </summary>
|
|
public KeyframesEditor()
|
|
{
|
|
_mainPanel = new Panel(ScrollBars.Both)
|
|
{
|
|
ScrollMargin = new Margin(150.0f),
|
|
AlwaysShowScrollbars = true,
|
|
AnchorPreset = AnchorPresets.StretchAll,
|
|
Offsets = Margin.Zero,
|
|
Parent = this
|
|
};
|
|
_contents = new Contents(this)
|
|
{
|
|
ClipChildren = false,
|
|
AutoFocus = false,
|
|
Parent = _mainPanel
|
|
};
|
|
|
|
UpdateKeyframes();
|
|
}
|
|
|
|
private void OnEdited()
|
|
{
|
|
Edited?.Invoke();
|
|
}
|
|
|
|
private void OnEditingStart()
|
|
{
|
|
EditingStart?.Invoke();
|
|
}
|
|
|
|
private void OnEditingEnd()
|
|
{
|
|
EditingEnd?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the keyframe value at the specified time.
|
|
/// </summary>
|
|
/// <param name="time">The time to evaluate the keyframe value.</param>
|
|
/// <returns>The evaluated value.</returns>
|
|
public object Evaluate(float time)
|
|
{
|
|
if (_keyframes.Count == 0)
|
|
return DefaultValue;
|
|
|
|
// Find the keyframe at time
|
|
int start = 0;
|
|
int searchLength = _keyframes.Count;
|
|
while (searchLength > 0)
|
|
{
|
|
int half = searchLength >> 1;
|
|
int mid = start + half;
|
|
|
|
if (time < _keyframes[mid].Time)
|
|
{
|
|
searchLength = half;
|
|
}
|
|
else
|
|
{
|
|
start = mid + 1;
|
|
searchLength -= (half + 1);
|
|
}
|
|
}
|
|
int leftKey = Mathf.Max(0, start - 1);
|
|
|
|
return _keyframes[leftKey].Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the keyframes collection.
|
|
/// </summary>
|
|
public void ResetKeyframes()
|
|
{
|
|
if (_keyframes.Count == 0)
|
|
return;
|
|
|
|
_keyframes.Clear();
|
|
|
|
UpdateFPS();
|
|
|
|
OnKeyframesChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the keyframes collection.
|
|
/// </summary>
|
|
/// <param name="keyframes">The keyframes.</param>
|
|
public void SetKeyframes(IEnumerable<Keyframe> keyframes)
|
|
{
|
|
if (keyframes == null)
|
|
throw new ArgumentNullException(nameof(keyframes));
|
|
var keyframesArray = keyframes as Keyframe[] ?? keyframes.ToArray();
|
|
if (_keyframes.SequenceEqual(keyframesArray))
|
|
return;
|
|
if (keyframesArray.Length > MaxKeyframes)
|
|
{
|
|
var tmp = keyframesArray;
|
|
keyframesArray = new Keyframe[MaxKeyframes];
|
|
Array.Copy(tmp, keyframesArray, MaxKeyframes);
|
|
}
|
|
|
|
_keyframes.Clear();
|
|
_keyframes.AddRange(keyframesArray);
|
|
_keyframes.Sort((a, b) => a.Time.CompareTo(b.Time));
|
|
|
|
UpdateFPS();
|
|
OnKeyframesChanged();
|
|
}
|
|
|
|
private void UpdateFPS()
|
|
{
|
|
if (FPS.HasValue)
|
|
{
|
|
float fps = FPS.Value;
|
|
for (int i = 0; i < _keyframes.Count; i++)
|
|
{
|
|
var k = _keyframes[i];
|
|
k.Time = Mathf.Floor(k.Time * fps) / fps;
|
|
_keyframes[i] = k;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when keyframes collection gets changed (keyframe added or removed).
|
|
/// </summary>
|
|
protected virtual void OnKeyframesChanged()
|
|
{
|
|
while (_points.Count > _keyframes.Count)
|
|
{
|
|
var last = _points.Count - 1;
|
|
_points[last].Dispose();
|
|
_points.RemoveAt(last);
|
|
}
|
|
|
|
while (_points.Count < _keyframes.Count)
|
|
{
|
|
_points.Add(new KeyframePoint
|
|
{
|
|
AutoFocus = false,
|
|
Size = KeyframesSize,
|
|
Editor = this,
|
|
Index = _points.Count,
|
|
Parent = _contents,
|
|
});
|
|
|
|
_refreshAfterEdit = true;
|
|
}
|
|
|
|
UpdateKeyframes();
|
|
|
|
KeyframesChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the new keyframe.
|
|
/// </summary>
|
|
/// <param name="k">The keyframe to add.</param>
|
|
public void AddKeyframe(Keyframe k)
|
|
{
|
|
var eps = Mathf.Epsilon;
|
|
if (FPS.HasValue)
|
|
{
|
|
float fps = FPS.Value;
|
|
k.Time = Mathf.Floor(k.Time * fps) / fps;
|
|
eps = 1.0f / fps;
|
|
}
|
|
|
|
int pos = 0;
|
|
while (pos < _keyframes.Count && _keyframes[pos].Time < k.Time)
|
|
pos++;
|
|
if (_keyframes.Count > pos && Mathf.Abs(_keyframes[pos].Time - k.Time) < eps)
|
|
_keyframes[pos] = k;
|
|
else
|
|
_keyframes.Insert(pos, k);
|
|
|
|
OnKeyframesChanged();
|
|
OnEdited();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the existing keyframe value as boxed value.
|
|
/// </summary>
|
|
/// <param name="index">The keyframe index.</param>
|
|
/// <param name="value">The keyframe value.</param>
|
|
public void SetKeyframe(int index, object value)
|
|
{
|
|
var k = _keyframes[index];
|
|
k.Value = value;
|
|
_keyframes[index] = k;
|
|
|
|
OnKeyframesChanged();
|
|
OnEdited();
|
|
}
|
|
|
|
private void AddKeyframe(Float2 keyframesPos)
|
|
{
|
|
var k = new Keyframe
|
|
{
|
|
Time = keyframesPos.X,
|
|
Value = Utilities.Utils.CloneValue(DefaultValue),
|
|
};
|
|
OnEditingStart();
|
|
AddKeyframe(k);
|
|
OnEditingEnd();
|
|
}
|
|
|
|
class Popup : ContextMenuBase
|
|
{
|
|
private CustomEditorPresenter _presenter;
|
|
private KeyframesEditor _editor;
|
|
private List<int> _keyframeIndices;
|
|
private bool _isDirty;
|
|
|
|
public Popup(KeyframesEditor editor, object[] selection, List<int> keyframeIndices = null, float height = 140.0f)
|
|
: this(editor, height)
|
|
{
|
|
_presenter.Select(selection);
|
|
_presenter.OpenAllGroups();
|
|
_keyframeIndices = keyframeIndices;
|
|
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
|
|
throw new Exception();
|
|
}
|
|
|
|
private Popup(KeyframesEditor editor, float height = 140.0f)
|
|
{
|
|
_editor = editor;
|
|
const float width = 280.0f;
|
|
Size = new Float2(width, height);
|
|
var panel1 = new Panel(ScrollBars.Vertical)
|
|
{
|
|
Bounds = new Rectangle(0, 0.0f, width, height),
|
|
Parent = this
|
|
};
|
|
_presenter = new CustomEditorPresenter(null);
|
|
_presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
|
|
_presenter.Panel.IsScrollable = true;
|
|
_presenter.Panel.Parent = panel1;
|
|
_presenter.Modified += OnModified;
|
|
}
|
|
|
|
private void OnModified()
|
|
{
|
|
if (!_isDirty)
|
|
{
|
|
_editor.OnEditingStart();
|
|
}
|
|
_isDirty = true;
|
|
|
|
if (_keyframeIndices != null)
|
|
{
|
|
for (int i = 0; i < _presenter.SelectionCount; i++)
|
|
{
|
|
var keyframe = (Keyframe)_presenter.Selection[i];
|
|
var index = _keyframeIndices[i];
|
|
_editor._keyframes[index] = keyframe;
|
|
}
|
|
}
|
|
else if (_presenter.Selection[0] is AllKeyframesProxy proxy)
|
|
{
|
|
_editor.SetKeyframes(proxy.Keyframes);
|
|
}
|
|
|
|
_editor.UpdateFPS();
|
|
_editor.UpdateKeyframes();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnShow()
|
|
{
|
|
Focus();
|
|
|
|
base.OnShow();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Hide()
|
|
{
|
|
if (!Visible)
|
|
return;
|
|
|
|
Focus(null);
|
|
|
|
if (_isDirty)
|
|
{
|
|
_editor.OnEdited();
|
|
_editor.OnEditingEnd();
|
|
}
|
|
|
|
if (_editor._popup == this)
|
|
_editor._popup = null;
|
|
_presenter = null;
|
|
_keyframeIndices = null;
|
|
_editor = null;
|
|
|
|
base.Hide();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnKeyDown(KeyboardKeys key)
|
|
{
|
|
if (base.OnKeyDown(key))
|
|
return true;
|
|
|
|
if (key == KeyboardKeys.Escape)
|
|
{
|
|
Hide();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnDestroy()
|
|
{
|
|
_editor = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
|
|
sealed class AllKeyframesProxy
|
|
{
|
|
[HideInEditor, NoSerialize]
|
|
public KeyframesEditor Editor;
|
|
|
|
[Collection(CanReorderItems = false, Spacing = 10)]
|
|
public Keyframe[] Keyframes;
|
|
}
|
|
|
|
private void EditAllKeyframes(Control control, Float2 pos)
|
|
{
|
|
_popup = new Popup(this, new object[] { new AllKeyframesProxy { Editor = this, Keyframes = _keyframes.ToArray(), } }, null, 400.0f);
|
|
_popup.Show(control, pos);
|
|
}
|
|
|
|
private void EditKeyframes(Control control, Float2 pos)
|
|
{
|
|
var keyframeIndices = new List<int>();
|
|
for (int i = 0; i < _points.Count; i++)
|
|
{
|
|
var p = _points[i];
|
|
if (!p.IsSelected || keyframeIndices.Contains(p.Index))
|
|
continue;
|
|
keyframeIndices.Add(p.Index);
|
|
}
|
|
EditKeyframes(control, pos, keyframeIndices);
|
|
}
|
|
|
|
private void EditKeyframes(Control control, Float2 pos, List<int> keyframeIndices)
|
|
{
|
|
var selection = new object[keyframeIndices.Count];
|
|
for (int i = 0; i < keyframeIndices.Count; i++)
|
|
selection[i] = _keyframes[keyframeIndices[i]];
|
|
_popup = new Popup(this, selection, keyframeIndices);
|
|
_popup.Show(control, pos);
|
|
}
|
|
|
|
private void RemoveKeyframes()
|
|
{
|
|
if (KeyframesEditorContext != null)
|
|
KeyframesEditorContext.OnKeyframesDelete(this);
|
|
else
|
|
RemoveKeyframesInner();
|
|
}
|
|
|
|
private void CopyKeyframes(KeyframePoint point = null)
|
|
{
|
|
float? timeOffset = null;
|
|
if (point != null)
|
|
{
|
|
timeOffset = -point.Time;
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < _points.Count; i++)
|
|
{
|
|
if (_points[i].IsSelected)
|
|
{
|
|
timeOffset = -_points[i].Time;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
KeyframesEditorUtils.Copy(this, timeOffset);
|
|
}
|
|
|
|
private void RemoveKeyframesInner()
|
|
{
|
|
if (SelectionCount == 0)
|
|
return;
|
|
var keyframes = new Dictionary<int, Keyframe>(_keyframes.Count);
|
|
for (int i = 0; i < _points.Count; i++)
|
|
{
|
|
var p = _points[i];
|
|
if (!p.IsSelected)
|
|
{
|
|
keyframes[p.Index] = _keyframes[p.Index];
|
|
}
|
|
else
|
|
{
|
|
p.IsSelected = false;
|
|
}
|
|
}
|
|
|
|
OnEditingStart();
|
|
_keyframes.Clear();
|
|
_keyframes.AddRange(keyframes.Values);
|
|
OnKeyframesChanged();
|
|
OnEdited();
|
|
OnEditingEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the whole keyframes UI.
|
|
/// </summary>
|
|
public void ShowWholeKeyframes()
|
|
{
|
|
ViewScale = _mainPanel.Size / _contents.Size;
|
|
ViewOffset = -_mainPanel.ControlsBounds.Location;
|
|
UpdateKeyframes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the view.
|
|
/// </summary>
|
|
public void ResetView()
|
|
{
|
|
ViewScale = Float2.One;
|
|
ViewOffset = Float2.Zero;
|
|
UpdateKeyframes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the keyframes positioning.
|
|
/// </summary>
|
|
public virtual void UpdateKeyframes()
|
|
{
|
|
if (_points.Count == 0)
|
|
{
|
|
// No keyframes
|
|
_contents.Bounds = Rectangle.Empty;
|
|
return;
|
|
}
|
|
|
|
_mainPanel.LockChildrenRecursive();
|
|
|
|
// Place keyframes
|
|
var viewScale = ViewScale;
|
|
for (int i = 0; i < _points.Count; i++)
|
|
{
|
|
var p = _points[i];
|
|
var k = _keyframes[p.Index];
|
|
|
|
p.Size = new Float2(4.0f / viewScale.X, Height - 2.0f);
|
|
p.Location = new Float2(k.Time * UnitsPerSecond - p.Width * 0.5f, 1.0f);
|
|
p.UpdateTooltip();
|
|
}
|
|
|
|
// Calculate bounds
|
|
var bounds = _points[0].Bounds;
|
|
for (var i = 1; i < _points.Count; i++)
|
|
{
|
|
bounds = Rectangle.Union(bounds, _points[i].Bounds);
|
|
}
|
|
|
|
// Adjust contents bounds to fill the keyframes area
|
|
if (EnablePanning && !_contents._isMovingSelection)
|
|
{
|
|
_contents.Bounds = bounds;
|
|
}
|
|
|
|
// Offset the keyframes (parent container changed its location)
|
|
var posOffset = _contents.Location;
|
|
for (var i = 0; i < _points.Count; i++)
|
|
{
|
|
_points[i].Location -= posOffset;
|
|
}
|
|
|
|
_mainPanel.UnlockChildrenRecursive();
|
|
_mainPanel.PerformLayout();
|
|
}
|
|
|
|
private int SelectionCount
|
|
{
|
|
get
|
|
{
|
|
int result = 0;
|
|
for (int i = 0; i < _points.Count; i++)
|
|
if (_points[i].IsSelected)
|
|
result++;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the selection.
|
|
/// </summary>
|
|
public void ClearSelection()
|
|
{
|
|
for (int i = 0; i < _points.Count; i++)
|
|
{
|
|
_points[i].IsSelected = false;
|
|
}
|
|
}
|
|
|
|
private void BulkSelectUpdate(bool select = true)
|
|
{
|
|
for (int i = 0; i < _points.Count; i++)
|
|
{
|
|
_points[i].IsSelected = select;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects all keyframes.
|
|
/// </summary>
|
|
public void SelectAll()
|
|
{
|
|
BulkSelectUpdate(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deselects all keyframes.
|
|
/// </summary>
|
|
public void DeselectAll()
|
|
{
|
|
BulkSelectUpdate(false);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Draw()
|
|
{
|
|
// Hack to refresh UI after keyframes edit
|
|
if (_refreshAfterEdit)
|
|
{
|
|
_refreshAfterEdit = false;
|
|
UpdateKeyframes();
|
|
}
|
|
|
|
var style = Style.Current;
|
|
var rect = new Rectangle(Float2.Zero, Size);
|
|
|
|
// Draw selection rectangle
|
|
if (_contents._leftMouseDown && !_contents._isMovingSelection)
|
|
{
|
|
var selectionRect = Rectangle.FromPoints
|
|
(
|
|
_mainPanel.PointToParent(_contents.PointToParent(_contents._leftMouseDownPos)),
|
|
_mainPanel.PointToParent(_contents.PointToParent(_contents._mousePos))
|
|
);
|
|
Render2D.FillRectangle(selectionRect, style.Selection);
|
|
Render2D.DrawRectangle(selectionRect, style.SelectionBorder);
|
|
}
|
|
|
|
base.Draw();
|
|
|
|
// Draw border
|
|
if (ContainsFocus)
|
|
{
|
|
Render2D.DrawRectangle(rect, style.BackgroundSelected);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnSizeChanged()
|
|
{
|
|
base.OnSizeChanged();
|
|
|
|
UpdateKeyframes();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnKeyDown(KeyboardKeys key)
|
|
{
|
|
if (base.OnKeyDown(key))
|
|
return true;
|
|
|
|
InputOptions options = Editor.Instance.Options.Options.Input;
|
|
if (options.SelectAll.Process(this))
|
|
{
|
|
SelectAll();
|
|
return true;
|
|
}
|
|
else if (options.DeselectAll.Process(this))
|
|
{
|
|
DeselectAll();
|
|
return true;
|
|
}
|
|
else if (options.Delete.Process(this))
|
|
{
|
|
RemoveKeyframes();
|
|
return true;
|
|
}
|
|
else if (options.Copy.Process(this))
|
|
{
|
|
CopyKeyframes();
|
|
return true;
|
|
}
|
|
else if (options.Paste.Process(this))
|
|
{
|
|
KeyframesEditorUtils.Paste(this);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnDestroy()
|
|
{
|
|
// Clear references to the controls
|
|
_mainPanel = null;
|
|
_contents = null;
|
|
_popup = null;
|
|
|
|
// Cleanup
|
|
_points.Clear();
|
|
_keyframes.Clear();
|
|
KeyframesEditorContext = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IKeyframesEditorContext KeyframesEditorContext { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesDeselect(IKeyframesEditor editor)
|
|
{
|
|
ClearSelection();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
|
|
{
|
|
if (_keyframes.Count == 0)
|
|
return;
|
|
var selectionRect = Rectangle.FromPoints(_contents.PointFromParent(control, selection.UpperLeft), _contents.PointFromParent(control, selection.BottomRight));
|
|
_contents.UpdateSelection(ref selectionRect);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int OnKeyframesSelectionCount()
|
|
{
|
|
return SelectionCount;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesDelete(IKeyframesEditor editor)
|
|
{
|
|
RemoveKeyframesInner();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end)
|
|
{
|
|
if (SelectionCount == 0)
|
|
return;
|
|
location = _contents.PointFromParent(control, location);
|
|
if (start)
|
|
_contents.OnMoveStart(location);
|
|
else if (end)
|
|
_contents.OnMoveEnd(location);
|
|
else
|
|
_contents.OnMove(location);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
|
|
{
|
|
if (SelectionCount == 0 || DefaultValue == null)
|
|
return;
|
|
var offset = timeOffset ?? 0.0f;
|
|
data.AppendLine(KeyframesEditorUtils.CopyPrefix);
|
|
data.AppendLine(DefaultValue.GetType().FullName);
|
|
for (int i = 0; i < _keyframes.Count; i++)
|
|
{
|
|
if (!_points[i].IsSelected)
|
|
continue;
|
|
var k = _keyframes[i];
|
|
data.AppendLine((k.Time + offset).ToString(CultureInfo.InvariantCulture));
|
|
data.AppendLine(JsonSerializer.Serialize(k.Value).RemoveNewLine());
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
|
|
{
|
|
if (DefaultValue == null)
|
|
return;
|
|
if (index == -1)
|
|
{
|
|
if (editor == this)
|
|
index = 0;
|
|
else
|
|
return;
|
|
}
|
|
else if (index >= datas.Length)
|
|
return;
|
|
var data = datas[index];
|
|
var offset = timeOffset ?? 0.0f;
|
|
var eps = FPS.HasValue ? 0.5f / FPS.Value : Mathf.Epsilon;
|
|
try
|
|
{
|
|
var lines = data.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (lines.Length < 3 || lines.Length % 2 == 0)
|
|
return;
|
|
var type = TypeUtils.GetManagedType(lines[0]);
|
|
if (type == null)
|
|
throw new Exception($"Unknown type {lines[0]}.");
|
|
if (type != DefaultValue.GetType())
|
|
throw new Exception($"Mismatching keyframes data type {type.FullName} when pasting into {DefaultValue.GetType().FullName}.");
|
|
var count = (lines.Length - 1) / 2;
|
|
var modified = false;
|
|
index++;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var k = new Keyframe
|
|
{
|
|
Time = float.Parse(lines[i * 2 + 1], CultureInfo.InvariantCulture) + offset,
|
|
Value = JsonSerializer.Deserialize(lines[i * 2 + 2], type),
|
|
};
|
|
if (FPS.HasValue)
|
|
{
|
|
float fps = FPS.Value;
|
|
k.Time = Mathf.Floor(k.Time * fps) / fps;
|
|
}
|
|
int pos = 0;
|
|
while (pos < _keyframes.Count && _keyframes[pos].Time < k.Time)
|
|
pos++;
|
|
if (_keyframes.Count > pos && Mathf.Abs(_keyframes[pos].Time - k.Time) < eps)
|
|
{
|
|
// Skip if the keyframe value won't change
|
|
if (JsonSerializer.ValueEquals(_keyframes[pos].Value, k.Value))
|
|
continue;
|
|
}
|
|
|
|
if (!modified)
|
|
{
|
|
modified = true;
|
|
OnEditingStart();
|
|
}
|
|
|
|
AddKeyframe(k);
|
|
_points[pos].IsSelected = true;
|
|
}
|
|
if (modified)
|
|
OnEditingEnd();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Editor.LogWarning("Failed to paste keyframes.");
|
|
Editor.LogWarning(ex.Message);
|
|
Editor.LogWarning(ex);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesGet(string trackName, Action<string, float, object> get)
|
|
{
|
|
for (int i = 0; i < _keyframes.Count; i++)
|
|
{
|
|
var k = _keyframes[i];
|
|
get(trackName, k.Time, k);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnKeyframesSet(List<KeyValuePair<float, object>> keyframes)
|
|
{
|
|
OnEditingStart();
|
|
_keyframes.Clear();
|
|
if (keyframes != null)
|
|
{
|
|
foreach (var e in keyframes)
|
|
{
|
|
var k = (Keyframe)e.Value;
|
|
_keyframes.Add(new Keyframe(e.Key, k.Value));
|
|
}
|
|
}
|
|
OnKeyframesChanged();
|
|
OnEdited();
|
|
OnEditingEnd();
|
|
}
|
|
}
|
|
}
|