using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Timeline; namespace UnityEditor.Timeline { enum ManipulateEdges { Left, Right, Both } class SnapEngine { static readonly float k_MagnetInfluenceInPixels = 10.0f; class SnapInfo { public double time { get; set; } public bool showSnapHint { get; set; } public bool IsInInfluenceZone(double currentTime, WindowState state) { var pos = state.TimeToPixel(currentTime); var magnetPos = state.TimeToPixel(time); return Math.Abs(pos - magnetPos) < k_MagnetInfluenceInPixels; } } struct TimeBoundaries { public TimeBoundaries(double l, double r) { left = l; right = r; } public readonly double left; public readonly double right; public TimeBoundaries Translate(double d) { return new TimeBoundaries(left + d, right + d); } } public static bool displayDebugLayout; readonly IAttractable m_Attractable; readonly IAttractionHandler m_AttractionHandler; readonly ManipulateEdges m_ManipulateEdges; readonly WindowState m_State; double m_GrabbedTime; TimeBoundaries m_GrabbedTimes; TimeBoundaries m_CurrentTimes; readonly List m_Magnets = new List(); bool m_SnapEnabled; public SnapEngine(IAttractable attractable, IAttractionHandler attractionHandler, ManipulateEdges manipulateEdges, WindowState state, Vector2 mousePosition, IEnumerable snappables = null) { m_Attractable = attractable; m_ManipulateEdges = manipulateEdges; m_AttractionHandler = attractionHandler; m_State = state; m_CurrentTimes = m_GrabbedTimes = new TimeBoundaries(m_Attractable.start, m_Attractable.end); m_GrabbedTime = m_State.PixelToTime(mousePosition.x); // Add Time zero as Magnet AddMagnet(0.0, true, state); // Add current Time as Magnet // case1157280 only add current time as magnet if visible if (TimelineWindow.instance.currentMode.ShouldShowTimeCursor(m_State)) AddMagnet(state.editSequence.time, true, state); if (state.IsEditingASubTimeline()) { // Add start and end of evaluable range as Magnets // This includes the case where the master timeline has a fixed length var range = state.editSequence.GetEvaluableRange(); AddMagnet(range.start, true, state); AddMagnet(range.end, true, state); } else if (state.masterSequence.asset.durationMode == TimelineAsset.DurationMode.FixedLength) { // Add end sequence Time as Magnet AddMagnet(state.masterSequence.asset.duration, true, state); } if (snappables == null) snappables = GetVisibleSnappables(m_State); foreach (var snappable in snappables) { if (!attractable.ShouldSnapTo(snappable)) continue; var edges = snappable.SnappableEdgesFor(attractable, manipulateEdges); foreach (var edge in edges) AddMagnet(edge.time, edge.showSnapHint, state); } } public static IEnumerable GetVisibleSnappables(WindowState state) { Rect rect = TimelineWindow.instance.state.timeAreaRect; rect.height = float.MaxValue; return state.spacePartitioner.GetItemsInArea(rect).ToArray(); } void AddMagnet(double magnetTime, bool showSnapHint, WindowState state) { var magnet = m_Magnets.FirstOrDefault(m => m.time.Equals(magnetTime)); if (magnet == null) { if (IsMagnetInShownArea(magnetTime, state)) m_Magnets.Add(new SnapInfo { time = magnetTime, showSnapHint = showSnapHint }); } else { magnet.showSnapHint |= showSnapHint; } } static bool IsMagnetInShownArea(double time, WindowState state) { var shownArea = state.timeAreaShownRange; return time >= shownArea.x && time <= shownArea.y; } SnapInfo GetMagnetAt(double time) { return m_Magnets.FirstOrDefault(m => m.time.Equals(time)); } SnapInfo ClosestMagnet(double time) { SnapInfo candidate = null; var min = double.MaxValue; foreach (var magnetInfo in m_Magnets) { var m = Math.Abs(magnetInfo.time - time); if (m < min) { candidate = magnetInfo; min = m; } } if (candidate != null && candidate.IsInInfluenceZone(time, m_State)) return candidate; return null; } public void Snap(Vector2 currentMousePosition, EventModifiers modifiers) { var d = m_State.PixelToTime(currentMousePosition.x) - m_GrabbedTime; m_CurrentTimes = m_GrabbedTimes.Translate(d); bool isLeft = m_ManipulateEdges == ManipulateEdges.Left || m_ManipulateEdges == ManipulateEdges.Both; bool isRight = m_ManipulateEdges == ManipulateEdges.Right || m_ManipulateEdges == ManipulateEdges.Both; bool attracted = false; m_SnapEnabled = modifiers == ManipulatorsUtils.actionModifier ? !m_State.edgeSnaps : m_State.edgeSnaps; if (m_SnapEnabled) { SnapInfo leftActiveMagnet = null; SnapInfo rightActiveMagnet = null; if (isLeft) leftActiveMagnet = ClosestMagnet(m_CurrentTimes.left); if (isRight) rightActiveMagnet = ClosestMagnet(m_CurrentTimes.right); if (leftActiveMagnet != null || rightActiveMagnet != null) { attracted = true; bool leftAttraction = false; if (rightActiveMagnet == null) { // Attracted by a left magnet only. leftAttraction = true; } else { if (leftActiveMagnet != null) { // Attracted by both magnets, choose the closest one. var leftDistance = Math.Abs(leftActiveMagnet.time - m_CurrentTimes.left); var rightDistance = Math.Abs(rightActiveMagnet.time - m_CurrentTimes.right); leftAttraction = leftDistance <= rightDistance; } // else, Attracted by right magnet only } if (leftAttraction) { m_AttractionHandler.OnAttractedEdge(m_Attractable, m_ManipulateEdges, AttractedEdge.Left, leftActiveMagnet.time); } else { m_AttractionHandler.OnAttractedEdge(m_Attractable, m_ManipulateEdges, AttractedEdge.Right, rightActiveMagnet.time); } } } if (!attracted) { var time = isLeft ? m_CurrentTimes.left : m_CurrentTimes.right; time = TimeReferenceUtility.SnapToFrameIfRequired(time); m_AttractionHandler.OnAttractedEdge(m_Attractable, m_ManipulateEdges, AttractedEdge.None, time); } } public void OnGUI(bool showLeft = true, bool showRight = true) { if (displayDebugLayout) { // Display Magnet influence zone foreach (var m in m_Magnets) { var window = TimelineWindow.instance; var rect = new Rect(m_State.TimeToPixel(m.time) - k_MagnetInfluenceInPixels, window.state.timeAreaRect.yMax, 2f * k_MagnetInfluenceInPixels, m_State.windowHeight); EditorGUI.DrawRect(rect, new Color(1f, 0f, 0f, 0.4f)); } // Display Cursor position var mousePos = Event.current.mousePosition; var time = m_State.PixelToTime(mousePos.x); var p = new Vector2(m_State.TimeToPixel(time), TimelineWindow.instance.state.timeAreaRect.yMax); var s = new Vector2(1f, m_State.windowHeight); EditorGUI.DrawRect(new Rect(p, s), Color.blue); p = new Vector2(m_State.TimeToPixel(m_GrabbedTime), TimelineWindow.instance.state.timeAreaRect.yMax); s = new Vector2(1f, m_State.windowHeight); EditorGUI.DrawRect(new Rect(p, s), Color.red); p = new Vector2(m_State.TimeToPixel(m_CurrentTimes.left), TimelineWindow.instance.state.timeAreaRect.yMax); s = new Vector2(1f, m_State.windowHeight); EditorGUI.DrawRect(new Rect(p, s), Color.yellow); p = new Vector2(m_State.TimeToPixel(m_CurrentTimes.right), TimelineWindow.instance.state.timeAreaRect.yMax); EditorGUI.DrawRect(new Rect(p, s), Color.yellow); } if (m_SnapEnabled) { if (showLeft) DrawMagnetLineAt(m_Attractable.start); if (showRight) DrawMagnetLineAt(m_Attractable.end); } } void DrawMagnetLineAt(double time) { var magnet = GetMagnetAt(time); if (magnet != null && magnet.showSnapHint) Graphics.DrawLineAtTime(m_State, magnet.time, Color.white); } } }