private void DoLoopCounterGUI(ref Rect area, float length) { if (_LoopCounterCache == null) { _LoopCounterCache = new ConversionCache <int, string>((x) => "x" + x); } string label; var normalizedTime = Target.Time / length; if (float.IsNaN(normalizedTime)) { label = "NaN"; } else { var loops = (int)Math.Abs(Target.Time / length); label = _LoopCounterCache.Convert(loops); } var width = AnimancerGUI.CalculateLabelWidth(label); var labelArea = AnimancerGUI.StealFromRight(ref area, width); GUI.Label(labelArea, label); }
/// <summary>Draws two float fields.</summary> public static float DoTwinFloatFieldGUI(Rect area, GUIContent label, float value, float normalizeMultiplier, bool isNormalized) { if (_PixelSuffixCache == null) { _PixelSuffixCache = new ConversionCache <float, string>((s) => s + "px"); _NormalizedSuffixCache = new ConversionCache <float, string>((x) => x + "x"); } var split = (area.width - EditorGUIUtility.labelWidth - AnimancerGUI.StandardSpacing) * 0.5f; var normalizedArea = AnimancerGUI.StealFromRight(ref area, Mathf.Floor(split), AnimancerGUI.StandardSpacing); var pixels = isNormalized ? value / normalizeMultiplier : value; var normalized = isNormalized ? value : value * normalizeMultiplier; EditorGUI.BeginChangeCheck(); pixels = AnimancerGUI.DoSpecialFloatField(area, label, pixels, _PixelSuffixCache); if (EditorGUI.EndChangeCheck()) { value = isNormalized ? pixels * normalizeMultiplier : pixels; } EditorGUI.BeginChangeCheck(); normalized = AnimancerGUI.DoSpecialFloatField(normalizedArea, null, normalized, _NormalizedSuffixCache); if (EditorGUI.EndChangeCheck()) { value = isNormalized ? normalized : normalized / normalizeMultiplier; } return(value); }
/// <summary> /// Draws a label showing the `weight` aligned to the right side of the `area` and reduces its /// <see cref="Rect.width"/> to remove that label from its area. /// </summary> public static void DoWeightLabel(ref Rect area, float weight) { string label; if (weight < 0) { label = "-?"; } else { if (_F1Cache == null) { _F1Cache = new ConversionCache <float, string>((value) => value.ToString("F1")); } label = _F1Cache.Convert(weight); } var style = ObjectPool.GetCachedResult(() => new GUIStyle(GUI.skin.label)); if (_WeightValueWidth < 0) { _WeightValueWidth = style.CalculateWidth("0.0"); } style.normal.textColor = Color.Lerp(Color.grey, TextColor, weight); style.fontStyle = Mathf.Approximately(weight * 10, (int)(weight * 10)) ? FontStyle.Normal : FontStyle.Italic; var weightArea = StealFromRight(ref area, _WeightValueWidth); GUI.Label(weightArea, label, style); }
/// <summary>[Animancer Extension] /// Calls <see cref="Gather(ICollection{AnimationClip}, AnimationClip)"/> for each clip in the `asset`. /// </summary> public static void GatherFromAsset(this ICollection <AnimationClip> clips, PlayableAsset asset) { if (asset == null) { return; } // We want to get the tracks out of a TimelineAsset without actually referencing that class directly // because it comes from an optional package and Animancer does not need to depend on that package. if (_TypeToGetRootTracks == null) { _TypeToGetRootTracks = new Editor.ConversionCache <Type, MethodInfo>((type) => { var method = type.GetMethod("GetRootTracks"); if (method != null && typeof(IEnumerable).IsAssignableFrom(method.ReturnType) && method.GetParameters().Length == 0) { return(method); } else { return(null); } }); } var getRootTracks = _TypeToGetRootTracks.Convert(asset.GetType()); if (getRootTracks != null) { var rootTracks = getRootTracks.Invoke(asset, null); GatherAnimationClips(rootTracks as IEnumerable, clips); } }
private static void GetEventLabels(int index, Context context, out GUIContent timeLabel, out string callbackLabel, out float defaultTime, out bool isEndEvent) { if (index >= context.TimeCount - 1) { timeLabel = AnimancerGUI.TempContent("End Time", Strings.ProOnlyTag + "The time when the end callback will be triggered"); callbackLabel = "End Callback"; defaultTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime( context.TransitionContext.Transition.Speed); isEndEvent = true; } else { if (_CallbackLabelCache == null) { _CallbackLabelCache = new ConversionCache <int, string>((i) => "Event " + i + " Callback"); _TimeLabelCache = new ConversionCache <int, string>((i) => "Event " + i + " Time"); } timeLabel = AnimancerGUI.TempContent(_TimeLabelCache.Convert(index), Strings.ProOnlyTag + "The time when the callback will be triggered"); callbackLabel = _CallbackLabelCache.Convert(index); defaultTime = 0; isEndEvent = false; } }
private static void GetEventLabels(int index, Context context, out string nameLabel, out string timeLabel, out string callbackLabel, out float defaultTime, out bool isEndEvent) { if (index >= context.Times.Count - 1) { nameLabel = null; timeLabel = "End Time"; callbackLabel = "End Callback"; defaultTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime( context.TransitionContext?.Transition?.Speed ?? 1); isEndEvent = true; } else { if (_NameLabelCache == null) { _NameLabelCache = new ConversionCache <int, string>((i) => $"Event {i} Name"); _TimeLabelCache = new ConversionCache <int, string>((i) => $"Event {i} Time"); _CallbackLabelCache = new ConversionCache <int, string>((i) => $"Event {i} Callback"); } nameLabel = _NameLabelCache.Convert(index); timeLabel = _TimeLabelCache.Convert(index); callbackLabel = _CallbackLabelCache.Convert(index); defaultTime = 0; isEndEvent = false; } }
/************************************************************************************************************************/ /// <summary>Creates and returns a cache for the specified `characterCount`.</summary> private ConversionCache <float, string> GetCache(int characterCount) { while (Caches.Count <= characterCount) { Caches.Add(null); } var cache = Caches[characterCount]; if (cache == null) { if (characterCount == 0) { cache = new ConversionCache <float, string>((value) => { return(value.ToStringCached() + Suffix); }); } else { cache = new ConversionCache <float, string>((value) => { var valueString = value.ToStringCached(); if (value > LargeExponentialThreshold || value < -LargeExponentialThreshold) { goto IsExponential; } if (_DecimalSeparator == null) { _DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; } var decimalIndex = valueString.IndexOf(_DecimalSeparator); if (decimalIndex < 0 || decimalIndex > characterCount) { goto IsExponential; } // Not exponential. return(valueString.Substring(0, characterCount) + ApproximateSuffix); IsExponential: var digits = Math.Max(0, characterCount - ApproximateSuffix.Length - 1); var format = GetExponentialFormat(digits); valueString = value.ToString(format); TrimExponential(ref valueString); return(valueString + Suffix); }); } Caches[characterCount] = cache; } return(cache); }
/// <summary> /// Calls <see cref="GUIStyle.CalcMinMaxWidth"/> using <see cref="GUISkin.label"/> and returns the max /// width. The result is cached for efficient reuse. /// <para></para> /// This method uses the <see cref="TempContent(string, string, bool)"/>. /// </summary> public static float CalculateLabelWidth(string text) { if (_LabelWidthCache == null) { _LabelWidthCache = CreateWidthCache(GUI.skin.label); } return(_LabelWidthCache.Convert(text)); }
private static void DrawSpriteFrames(AnimationClip clip) { var keyframes = GetSpriteReferences(clip); if (keyframes == null) { return; } for (int i = 0; i < keyframes.Length; i++) { var keyframe = keyframes[i]; var sprite = keyframe.value as Sprite; if (sprite != null) { if (_FrameCache == null) { _FrameCache = new ConversionCache <int, string>( (value) => $"Frame: {value}"); _TimeCache = new ConversionCache <float, string>( (value) => $"Time: {value}s"); } var texture = sprite.texture; var area = GUILayoutUtility.GetRect(0, AnimancerGUI.LineHeight * 4); var width = area.width; var rect = sprite.rect; area.width = area.height * rect.width / rect.height; rect.x /= texture.width; rect.y /= texture.height; rect.width /= texture.width; rect.height /= texture.height; GUI.DrawTextureWithTexCoords(area, texture, rect); var offset = area.width + AnimancerGUI.StandardSpacing; area.x += offset; area.width = width - offset; area.height = AnimancerGUI.LineHeight; area.y += Mathf.Round(area.height * 0.5f); GUI.Label(area, _FrameCache.Convert(i)); AnimancerGUI.NextVerticalArea(ref area); GUI.Label(area, _TimeCache.Convert(keyframe.time)); AnimancerGUI.NextVerticalArea(ref area); GUI.Label(area, sprite.name); } } }
private void DoRulerLabelGUI(ref Rect previousArea, float time) { if (_RulerLabelStyle == null) { _RulerLabelStyle = new GUIStyle(GUI.skin.label) { padding = new RectOffset(), contentOffset = new Vector2(0, -2), alignment = TextAnchor.UpperLeft, fontSize = Mathf.CeilToInt(AnimancerGUI.LineHeight * 0.6f), } } ; var text = G2Cache.Convert(time); if (_TimeLabelWidthCache == null) { _TimeLabelWidthCache = AnimancerGUI.CreateWidthCache(_RulerLabelStyle); } var area = new Rect( SecondsToPixels(time), _Area.y, _TimeLabelWidthCache.Convert(text), _Area.height); if (area.x > _Area.x) { var tickY = _Area.yMax - TickHeight; EditorGUI.DrawRect(new Rect(area.x, tickY, 1, TickHeight), AnimancerGUI.TextColor); } if (area.xMax > _Area.xMax) { area.x = _Area.xMax - area.width; } if (area.x < 0) { area.x = 0; } if (area.x > previousArea.xMax + 2) { GUI.Label(area, text, _RulerLabelStyle); previousArea = area; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ }
/// <summary> /// Draw a <see cref="EditorGUI.FloatField(Rect, GUIContent, float)"/> with an alternate cached string when it /// is not selected (for example, "1" might become "1s" to indicate "seconds"). /// </summary> public static void DoFloatFieldWithSuffix(Rect area, GUIContent label, SerializedProperty property, ConversionCache <float, string> toString) { label = EditorGUI.BeginProperty(area, label, property); EditorGUI.BeginChangeCheck(); var value = DoSpecialFloatField(area, label, property.floatValue, toString); if (EditorGUI.EndChangeCheck()) { property.floatValue = value; } EditorGUI.EndProperty(); }
/// <summary> /// Returns the `text` without any spaces if <see cref="EditorGUIUtility.wideMode"/> is false. /// Otherwise simply returns the `text` without any changes. /// </summary> public static string GetNarrowText(string text) { if (EditorGUIUtility.wideMode || string.IsNullOrEmpty(text)) { return(text); } if (_NarrowTextCache == null) { _NarrowTextCache = new ConversionCache <string, string>((str) => str.Replace(" ", "")); } return(_NarrowTextCache.Convert(text)); }
/************************************************************************************************************************/ /// <summary>Calculate the index of the cache to use for the given parameters.</summary> private int CalculateCacheIndex(float value, float width) { //if (value > LargeExponentialThreshold || // value < -LargeExponentialThreshold) // return 0; var valueString = value.ToStringCached(); // It the approximated string wouldn't be shorter than the original, don't approximate. if (valueString.Length < 2 + ApproximateSuffix.Length) { return(0); } if (_SuffixWidth == 0) { if (_WidthCache == null) { _WidthCache = AnimancerGUI.CreateWidthCache(EditorStyles.numberField); _FieldPadding = EditorStyles.numberField.padding.horizontal; _ApproximateSymbolWidth = _WidthCache.Convert("~") - _FieldPadding; } _SuffixWidth = _WidthCache.Convert(Suffix); } // If the field is wide enough to fit the full value, don't approximate. width -= _FieldPadding + _ApproximateSymbolWidth * 0.75f; var valueWidth = _WidthCache.Convert(valueString) + _SuffixWidth; if (valueWidth <= width) { return(0); } // If the number of allowed characters would include the full value, don't approximate. var suffixedLength = valueString.Length + Suffix.Length; var allowedCharacters = (int)(suffixedLength * width / valueWidth); if (allowedCharacters + 2 >= suffixedLength) { return(0); } return(allowedCharacters); }
/// <summary>Draws the GUI for the `animancerEvent`.</summary> public static void Draw(ref Rect area, string name, AnimancerEvent animancerEvent) { area.height = AnimancerGUI.LineHeight; if (_EventTimeCache == null) { _EventTimeCache = new ConversionCache <float, string>((time) => float.IsNaN(time) ? "Time = Auto" : $"Time = {time.ToStringCached()}x"); } EditorGUI.LabelField(area, name, _EventTimeCache.Convert(animancerEvent.normalizedTime)); AnimancerGUI.NextVerticalArea(ref area); EditorGUI.indentLevel++; DrawInvocationList(ref area, animancerEvent.callback); EditorGUI.indentLevel--; }
/// <summary> /// Draws a label showing the `weight` aligned to the right side of the `area` and reduces its /// <see cref="Rect.width"/> to remove that label from its area. /// </summary> public static void DoWeightLabel(ref Rect area, float weight) { if (_F1Cache == null) { _F1Cache = new ConversionCache <float, string>((value) => value.ToString("F1")); _WeightLabelStyle = new GUIStyle(GUI.skin.label); _WeightValueWidth = _WeightLabelStyle.CalculateWidth("0.0"); } var weightArea = StealFromRight(ref area, _WeightValueWidth); var label = _F1Cache.Convert(weight); _WeightLabelStyle.normal.textColor = Color.Lerp(Color.grey, TextColor, weight); _WeightLabelStyle.fontStyle = Mathf.Approximately(weight * 10, (int)(weight * 10)) ? FontStyle.Normal : FontStyle.Italic; GUI.Label(weightArea, label, _WeightLabelStyle); }
private void DoRulerLabelGUI(ref Rect previousArea, float time) { var text = G2Cache.Convert(time); if (_TimeLabelWidthCache == null) { _TimeLabelWidthCache = AnimancerGUI.CreateWidthCache(Styles.TimeLabel); } var area = new Rect( SecondsToPixels(time), _Area.y, _TimeLabelWidthCache.Convert(text), _Area.height); if (area.x > _Area.x) { var tickHeight = _Area.height * TickHeight; var tickY = _Area.yMax - tickHeight; EditorGUI.DrawRect(new Rect(area.x, tickY, 1, tickHeight), AnimancerGUI.TextColor); } if (area.xMax > _Area.xMax) { area.x = _Area.xMax - area.width; } if (area.x < 0) { area.x = 0; } if (area.x > previousArea.xMax + 2) { GUI.Label(area, text, Styles.TimeLabel); previousArea = area; } }
/// <summary>Gathers all the animations in the `tracks`.</summary> private static void GatherAnimationClips(IEnumerable tracks, ICollection <AnimationClip> clips) { if (tracks == null) { return; } if (_TrackAssetToGetClips == null) { _TrackAssetToGetClips = new Editor.ConversionCache <Type, MethodInfo>((type) => { var method = type.GetMethod("GetClips"); if (method != null && typeof(IEnumerable).IsAssignableFrom(method.ReturnType) && method.GetParameters().Length == 0) { return(method); } else { return(null); } }); _TimelineClipToAnimationClip = new Editor.ConversionCache <Type, MethodInfo>((type) => { var property = type.GetProperty("animationClip"); if (property != null && property.PropertyType == typeof(AnimationClip)) { return(property.GetGetMethod()); } else { return(null); } }); _TrackAssetToGetChildTracks = new Editor.ConversionCache <Type, MethodInfo>((type) => { var method = type.GetMethod("GetChildTracks"); if (method != null && typeof(IEnumerable).IsAssignableFrom(method.ReturnType) && method.GetParameters().Length == 0) { return(method); } else { return(null); } }); } foreach (var track in tracks) { if (track == null) { continue; } var trackType = track.GetType(); var getClips = _TrackAssetToGetClips.Convert(trackType); if (getClips != null) { var trackClips = getClips.Invoke(track, null) as IEnumerable; if (trackClips != null) { foreach (var clip in trackClips) { var getClip = _TimelineClipToAnimationClip.Convert(clip.GetType()); if (getClip != null) { clips.Gather(getClip.Invoke(clip, null) as AnimationClip); } } } } var getChildTracks = _TrackAssetToGetChildTracks.Convert(trackType); if (getChildTracks != null) { var childTracks = getChildTracks.Invoke(track, null); GatherAnimationClips(childTracks as IEnumerable, clips); } } }
/// <summary> /// Draw a <see cref="GUI.Toggle(Rect, bool, GUIContent)"/> which sets the value to <see cref="float.NaN"/> /// when disabled followed by two float fields to display the `time` both normalized and in seconds. /// </summary> public static float DoOptionalTimeField(ref Rect area, GUIContent label, float time, bool timeIsNormalized, float length, float defaultValue = 0, bool isOptional = true) { if (_XSuffixCache == null) { _XSuffixCache = new ConversionCache <float, string>((x) => x + "x"); _SSuffixCache = new ConversionCache <float, string>((s) => s + "s"); } area.height = LineHeight; bool showNormalized, showSeconds; if (length > 0) { showNormalized = showSeconds = true; } else { showNormalized = timeIsNormalized; showSeconds = !timeIsNormalized; } var labelWidth = EditorGUIUtility.labelWidth; var enabled = GUI.enabled; var toggleArea = area; if (isOptional) { toggleArea.x += EditorGUIUtility.labelWidth; toggleArea.width = ToggleWidth; EditorGUIUtility.labelWidth += toggleArea.width; EditorGUIUtility.AddCursorRect(toggleArea, MouseCursor.Arrow); // We need to draw the toggle after everything else to it goes on top of the label. But we want it to // get priority for input events, so we disable the other controls during those events in its area. var currentEvent = Event.current; if (enabled && toggleArea.Contains(currentEvent.mousePosition)) { switch (currentEvent.type) { case EventType.Repaint: case EventType.Layout: break; default: GUI.enabled = false; break; } } } else if (float.IsNaN(time)) { time = defaultValue; } var displayTime = float.IsNaN(time) ? defaultValue : time; var normalizedArea = area; var secondsArea = area; if (showNormalized) { if (showSeconds) { var split = (EditorGUIUtility.labelWidth + normalizedArea.xMax - StandardSpacing) * 0.5f; normalizedArea.xMax = split; secondsArea.xMin = split + StandardSpacing; } var normalizedTime = timeIsNormalized ? displayTime : displayTime / length; EditorGUI.BeginChangeCheck(); normalizedTime = DoSpecialFloatField(normalizedArea, label, normalizedTime, _XSuffixCache); if (EditorGUI.EndChangeCheck()) { time = timeIsNormalized ? normalizedTime : normalizedTime * length; } } EditorGUIUtility.labelWidth = labelWidth; if (showSeconds) { var rawTime = timeIsNormalized ? displayTime * length : displayTime; if (showNormalized) { label = null; } EditorGUI.BeginChangeCheck(); rawTime = DoSpecialFloatField(secondsArea, label, rawTime, _SSuffixCache); if (EditorGUI.EndChangeCheck()) { if (timeIsNormalized) { if (length != 0) { time = rawTime / length; } } else { time = rawTime; } } } GUI.enabled = enabled; if (isOptional) { DoOptionalTimeToggle(toggleArea, ref time, defaultValue); } return(time); }
/************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Fields /************************************************************************************************************************/ /// <summary> /// Draw a <see cref="EditorGUI.FloatField(Rect, GUIContent, float)"/> with an alternate cached string when it /// is not selected (for example, "1" might become "1s" to indicate "seconds"). /// </summary> public static float DoSpecialFloatField(Rect area, GUIContent label, float value, ConversionCache <float, string> toString) { // Treat most events normally, but when repainting show a text field with the cached string. if (label != null) { if (Event.current.type != EventType.Repaint) { return(EditorGUI.FloatField(area, label, value)); } var dragArea = new Rect(area.x, area.y, EditorGUIUtility.labelWidth, area.height); EditorGUIUtility.AddCursorRect(dragArea, MouseCursor.SlideArrow); EditorGUI.TextField(area, label, toString.Convert(value)); } else { var indentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; if (Event.current.type != EventType.Repaint) { value = EditorGUI.FloatField(area, value); } else { EditorGUI.TextField(area, toString.Convert(value)); } EditorGUI.indentLevel = indentLevel; } return(value); }
/// <summary>Returns a string which approximates the `weight` into no more than 3 digits.</summary> private static string WeightToShortString(float weight, out bool isExact) { isExact = true; if (weight == 0) { return("0.0"); } if (weight == 1) { return("1.0"); } isExact = false; if (weight >= -0.5f && weight < 0.05f) { return("~0."); } if (weight >= 0.95f && weight < 1.05f) { return("~1."); } if (weight <= -99.5f) { return("-??"); } if (weight >= 999.5f) { return("???"); } if (_ShortWeightCache == null) { _ShortWeightCache = new ConversionCache <float, string>((value) => { if (value < -9.5f) { return($"{value:F0}"); } if (value < -0.5f) { return($"{value:F0}."); } if (value < 9.5f) { return($"{value:F1}"); } if (value < 99.5f) { return($"{value:F0}."); } return($"{value:F0}"); }); } var rounded = weight > 0 ? Mathf.Floor(weight * 10) : Mathf.Ceil(weight * 10); isExact = Mathf.Approximately(weight * 10, rounded); return(_ShortWeightCache.Convert(weight)); }
/************************************************************************************************************************/ /// <summary>Draws the GUI for the `events`.</summary> public void Draw(ref Rect area, Sequence events, GUIContent label) { if (events == null) { return; } area.height = AnimancerGUI.LineHeight; var headerArea = area; const string LogLabel = "Log"; if (float.IsNaN(_LogButtonWidth)) { _LogButtonWidth = EditorStyles.miniButton.CalculateWidth(LogLabel); } var logArea = AnimancerGUI.StealFromRight(ref headerArea, _LogButtonWidth); if (GUI.Button(logArea, LogLabel, EditorStyles.miniButton)) { Debug.Log(events.DeepToString()); } _IsExpanded = EditorGUI.Foldout(headerArea, _IsExpanded, GUIContent.none, true); using (ObjectPool.Disposable.AcquireContent(out var summary, GetSummary(events))) EditorGUI.LabelField(headerArea, label, summary); AnimancerGUI.NextVerticalArea(ref area); if (!_IsExpanded) { return; } var enabled = GUI.enabled; GUI.enabled = false; EditorGUI.indentLevel++; for (int i = 0; i < events.Count; i++) { var name = events.GetName(i); if (string.IsNullOrEmpty(name)) { if (_EventNumberCache == null) { _EventNumberCache = new ConversionCache <int, string>((index) => $"Event {index}"); } name = _EventNumberCache.Convert(i); } Draw(ref area, name, events[i]); } Draw(ref area, "End Event", events.endEvent); EditorGUI.indentLevel--; GUI.enabled = enabled; }