Merge remote-tracking branch 'origin/master' into 1.13
# Conflicts: # Source/Editor/GUI/CurveEditor.Contents.cs
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -55,6 +55,21 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
private Window _window;
|
||||
private Control _previouslyFocused;
|
||||
|
||||
private static bool IsFinite(float value)
|
||||
{
|
||||
return !float.IsNaN(value) && !float.IsInfinity(value);
|
||||
}
|
||||
|
||||
private static bool IsValidWindowBounds(Float2 location, Float2 size)
|
||||
{
|
||||
return IsFinite(location.X) &&
|
||||
IsFinite(location.Y) &&
|
||||
IsFinite(size.X) &&
|
||||
IsFinite(size.Y) &&
|
||||
size.X > 0.0f &&
|
||||
size.Y > 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether use automatic popup direction fix based on the screen dimensions.
|
||||
/// </summary>
|
||||
@@ -190,6 +205,11 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
var dpiSize = Size * dpiScale;
|
||||
var locationWS = parent.PointToWindow(location);
|
||||
var locationSS = parentWin.PointToScreen(locationWS);
|
||||
if (!IsValidWindowBounds(locationSS, dpiSize))
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
var monitorBounds = Platform.GetMonitorBounds(locationSS);
|
||||
var rightBottomLocationSS = locationSS + dpiSize;
|
||||
bool isUp = false, isLeft = false;
|
||||
@@ -405,7 +425,31 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.ClientSize = Size * _window.DpiScale;
|
||||
var size = Size * _window.DpiScale;
|
||||
if (!IsValidWindowBounds(_window.ClientBounds.Location, size))
|
||||
return;
|
||||
_window.ClientSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the popup window location in screen coordinates.
|
||||
/// </summary>
|
||||
protected Float2 WindowLocation => _window != null ? _window.ClientBounds.Location : Float2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Moves the popup window to the given screen-space location.
|
||||
/// </summary>
|
||||
/// <param name="screenLocation">The popup window location in screen coordinates.</param>
|
||||
protected void MoveWindowTo(Float2 screenLocation)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
var bounds = _window.ClientBounds;
|
||||
if (!IsValidWindowBounds(screenLocation, bounds.Size))
|
||||
return;
|
||||
bounds.Location = screenLocation;
|
||||
_window.ClientBounds = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,28 @@ namespace FlaxEditor.GUI
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
internal static string GetComponentLabel(int component, Type valueType)
|
||||
{
|
||||
if (valueType == typeof(Color) || valueType == typeof(Color32))
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case 0: return "r";
|
||||
case 1: return "g";
|
||||
case 2: return "b";
|
||||
case 3: return "a";
|
||||
}
|
||||
}
|
||||
switch (component)
|
||||
{
|
||||
case 0: return "x";
|
||||
case 1: return "y";
|
||||
case 2: return "z";
|
||||
case 3: return "w";
|
||||
default: return (component + 1).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the animation curve value at the specified time.
|
||||
/// </summary>
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace FlaxEditor.GUI
|
||||
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
||||
protected class ContentsBase : ContainerControl
|
||||
{
|
||||
private const float DragStartDistance = 1.5f;
|
||||
private const float DragStartDistanceSquared = DragStartDistance * DragStartDistance;
|
||||
|
||||
private enum SelectionMode
|
||||
{
|
||||
Replace,
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
private readonly CurveEditor<T> _editor;
|
||||
internal bool _leftMouseDown;
|
||||
private bool _rightMouseDown;
|
||||
@@ -46,6 +56,77 @@ namespace FlaxEditor.GUI
|
||||
_editor = editor;
|
||||
}
|
||||
|
||||
private KeyframePoint GetKeyframePointAt(Float2 location, bool cycle)
|
||||
{
|
||||
return GetKeyframePointAt(location, cycle, out _);
|
||||
}
|
||||
|
||||
private KeyframePoint GetKeyframePointAt(Float2 location, bool cycle, out bool isStacked)
|
||||
{
|
||||
KeyframePoint firstHit = null;
|
||||
KeyframePoint selectedHit = null;
|
||||
KeyframePoint nextHitAfterSelected = null;
|
||||
int hitsCount = 0;
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var point = _editor._points[i];
|
||||
if (!point.Visible || !point.Bounds.Contains(ref location))
|
||||
continue;
|
||||
|
||||
if (hitsCount == 0)
|
||||
firstHit = point;
|
||||
if (selectedHit != null && nextHitAfterSelected == null)
|
||||
nextHitAfterSelected = point;
|
||||
if (selectedHit == null && point.IsSelected)
|
||||
selectedHit = point;
|
||||
hitsCount++;
|
||||
}
|
||||
isStacked = hitsCount > 1;
|
||||
if (hitsCount == 0)
|
||||
return null;
|
||||
if (hitsCount == 1)
|
||||
return firstHit;
|
||||
if (selectedHit != null)
|
||||
return cycle ? nextHitAfterSelected ?? firstHit : selectedHit;
|
||||
return firstHit;
|
||||
}
|
||||
|
||||
private void SelectKeyframePoint(KeyframePoint keyframe, bool addToSelection)
|
||||
{
|
||||
if (!addToSelection)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
}
|
||||
keyframe.IsSelected = true;
|
||||
if (_editor.ShowCollapsed)
|
||||
{
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var point = _editor._points[i];
|
||||
if (point.Index == keyframe.Index)
|
||||
point.IsSelected = true;
|
||||
}
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
|
||||
private void SelectKeyframeComponent(int keyframeIndex, int component)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var point = _editor._points[i];
|
||||
point.IsSelected = point.Index == keyframeIndex && point.Component == component;
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
|
||||
private void UpdateSelectionRectangle()
|
||||
{
|
||||
var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos);
|
||||
@@ -55,14 +136,41 @@ namespace FlaxEditor.GUI
|
||||
UpdateSelection(ref selectionRect);
|
||||
}
|
||||
|
||||
private SelectionMode GetSelectionMode()
|
||||
{
|
||||
if (Root.GetKey(KeyboardKeys.Alt))
|
||||
return SelectionMode.Remove;
|
||||
if (Root.GetKey(KeyboardKeys.Shift))
|
||||
return SelectionMode.Add;
|
||||
return SelectionMode.Replace;
|
||||
}
|
||||
|
||||
internal void UpdateSelection(ref Rectangle selectionRect)
|
||||
{
|
||||
var mode = GetSelectionMode();
|
||||
|
||||
// Find controls to select
|
||||
var children = _children;
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
if (children[i] is KeyframePoint p)
|
||||
p.IsSelected = p.Bounds.Intersects(ref selectionRect);
|
||||
{
|
||||
var intersects = p.Bounds.Intersects(ref selectionRect);
|
||||
switch (mode)
|
||||
{
|
||||
case SelectionMode.Replace:
|
||||
p.IsSelected = intersects;
|
||||
break;
|
||||
case SelectionMode.Add:
|
||||
if (intersects)
|
||||
p.IsSelected = true;
|
||||
break;
|
||||
case SelectionMode.Remove:
|
||||
if (intersects)
|
||||
p.IsSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
@@ -82,19 +190,15 @@ namespace FlaxEditor.GUI
|
||||
_editor.OnEditingStart();
|
||||
}
|
||||
|
||||
internal void OnMove(Float2 location)
|
||||
private bool MoveSelectedKeyframes(Float2 location)
|
||||
{
|
||||
// Skip updating keyframes until move actual starts to be meaningful
|
||||
if (Float2.Distance(ref _movingSelectionStartPosLock, ref location) < 1.5f)
|
||||
return;
|
||||
_movingSelectionStartPosLock = Float2.Minimum;
|
||||
|
||||
var viewRect = _editor._mainPanel.GetClientArea();
|
||||
var locationKeyframes = PointToKeyframes(location, ref viewRect);
|
||||
var accessor = _editor.Accessor;
|
||||
var components = accessor.GetCurveComponents();
|
||||
var snapEnabled = Root.GetKey(KeyboardKeys.Control);
|
||||
var snapGrid = snapEnabled ? _editor.GetGridSnap() : Float2.One;
|
||||
var moved = false;
|
||||
for (var i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var p = _editor._points[i];
|
||||
@@ -154,7 +258,23 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
|
||||
_editor.SetKeyframeInternal(p.Index, time, value, p.Component);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
internal void OnMove(Float2 location)
|
||||
{
|
||||
// Skip updating keyframes until move actual starts to be meaningful
|
||||
if (Float2.Distance(ref _movingSelectionStartPosLock, ref location) < 1.5f)
|
||||
return;
|
||||
_movingSelectionStartPosLock = Float2.Minimum;
|
||||
|
||||
var moved = MoveSelectedKeyframes(location);
|
||||
if (moved)
|
||||
{
|
||||
_editor.UpdateKeyframes();
|
||||
_editor.UpdateTooltips();
|
||||
if (_editor.EnablePanning == UseMode.On)
|
||||
@@ -168,13 +288,15 @@ namespace FlaxEditor.GUI
|
||||
|
||||
internal void OnMoveEnd(Float2 location)
|
||||
{
|
||||
_isMovingSelection = false;
|
||||
if (_movedKeyframes)
|
||||
{
|
||||
_editor.OnEdited();
|
||||
_editor.OnEditingEnd();
|
||||
_editor.UpdateKeyframes();
|
||||
_editor.UpdateTooltips();
|
||||
_movedKeyframes = false;
|
||||
}
|
||||
_isMovingSelection = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -199,26 +321,32 @@ namespace FlaxEditor.GUI
|
||||
_mousePos = location;
|
||||
|
||||
// Start moving selection if movement started from the keyframe
|
||||
if (_leftMouseDown && !_isMovingSelection && GetChildAt(_leftMouseDownPos) is KeyframePoint)
|
||||
var leftMouseDownOverKeyframe = _leftMouseDown && GetKeyframePointAt(_leftMouseDownPos, false) != null;
|
||||
if (leftMouseDownOverKeyframe && !_isMovingSelection)
|
||||
{
|
||||
if (Float2.DistanceSquared(ref _leftMouseDownPos, ref location) < DragStartDistanceSquared)
|
||||
return;
|
||||
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, true, false);
|
||||
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, _leftMouseDownPos, true, false);
|
||||
else
|
||||
OnMoveStart(location);
|
||||
OnMoveStart(_leftMouseDownPos);
|
||||
}
|
||||
|
||||
// Moving view
|
||||
if (_rightMouseDown)
|
||||
{
|
||||
var movingViewPos = Parent.PointToParent(PointToParent(location));
|
||||
var delta = movingViewPos - _movingViewLastPos;
|
||||
if (_editor.CustomViewPanning != null)
|
||||
delta = _editor.CustomViewPanning(delta);
|
||||
delta *= GetUseModeMask(_editor.EnablePanning);
|
||||
if (delta.LengthSquared > 0.01f)
|
||||
var mousePosition = Root.MousePosition;
|
||||
var mouseDelta = mousePosition - _movingViewLastPos;
|
||||
if (mouseDelta.LengthSquared > 0.01f)
|
||||
{
|
||||
_editor._mainPanel.ViewOffset += delta;
|
||||
_movingViewLastPos = movingViewPos;
|
||||
var delta = mouseDelta;
|
||||
if (_editor.CustomViewPanning != null)
|
||||
delta = _editor.CustomViewPanning(delta);
|
||||
var viewDelta = delta * GetUseModeMask(_editor.EnablePanning);
|
||||
if (viewDelta.LengthSquared > 0.0f)
|
||||
_editor.ViewOffset += viewDelta;
|
||||
_movingViewLastPos = mousePosition;
|
||||
_movedView = true;
|
||||
if (_editor.CustomViewPanning != null)
|
||||
{
|
||||
@@ -257,7 +385,7 @@ namespace FlaxEditor.GUI
|
||||
var tangent = PointToKeyframes(location, ref viewRect).Y - value;
|
||||
if (Root.GetKey(KeyboardKeys.Control))
|
||||
tangent = Float2.SnapToGrid(new Float2(0, tangent), _editor.GetGridSnap()).Y; // Snap tangent over Y axis
|
||||
tangent = tangent * _editor.ViewScale.X * 2;
|
||||
tangent = tangent * UnitsPerSecond / _movingTangent.TangentOffset;
|
||||
_movingTangent.TangentValue = tangent;
|
||||
_editor.UpdateTangents();
|
||||
Cursor = CursorType.SizeNS;
|
||||
@@ -265,7 +393,7 @@ namespace FlaxEditor.GUI
|
||||
return;
|
||||
}
|
||||
// Selecting
|
||||
else if (_leftMouseDown)
|
||||
else if (_leftMouseDown && !leftMouseDownOverKeyframe)
|
||||
{
|
||||
UpdateSelectionRectangle();
|
||||
return;
|
||||
@@ -321,12 +449,13 @@ namespace FlaxEditor.GUI
|
||||
_rightMouseDown = true;
|
||||
_rightMouseDownPos = location;
|
||||
_movedView = false;
|
||||
_movingViewLastPos = Parent.PointToParent(PointToParent(location));
|
||||
_movingViewLastPos = Root.MousePosition;
|
||||
}
|
||||
|
||||
// Check if any node is under the mouse
|
||||
var underMouse = GetChildAt(location);
|
||||
if (underMouse is KeyframePoint keyframe)
|
||||
var keyframe = underMouse is KeyframePoint ? GetKeyframePointAt(location, false) : null;
|
||||
if (keyframe != null)
|
||||
{
|
||||
if (_leftMouseDown)
|
||||
{
|
||||
@@ -355,12 +484,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Select node
|
||||
if (!Root.GetKey(KeyboardKeys.Control))
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
}
|
||||
SelectKeyframePoint(keyframe, false);
|
||||
_toggledSelection = true;
|
||||
keyframe.IsSelected = true;
|
||||
_editor.UpdateTangents();
|
||||
@@ -402,11 +526,14 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Start selecting
|
||||
StartMouseCapture();
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
_editor.UpdateTangents();
|
||||
if (GetSelectionMode() == SelectionMode.Replace)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
@@ -453,11 +580,21 @@ namespace FlaxEditor.GUI
|
||||
OnMoveEnd(location);
|
||||
}
|
||||
// Toggle selection
|
||||
else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetChildAt(location) is KeyframePoint keyframe)
|
||||
else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetKeyframePointAt(location, false) is KeyframePoint keyframe)
|
||||
{
|
||||
keyframe.IsSelected = !keyframe.IsSelected;
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
// Select next stacked keyframe component only after a click has completed, not before a possible drag.
|
||||
else if (!_toggledSelection && !Root.GetKey(KeyboardKeys.Control) && !Root.GetKey(KeyboardKeys.Shift) &&
|
||||
Float2.DistanceSquared(ref _leftMouseDownPos, ref location) < DragStartDistanceSquared &&
|
||||
GetKeyframePointAt(_leftMouseDownPos, false, out var mouseDownStacked) != null &&
|
||||
mouseDownStacked &&
|
||||
GetKeyframePointAt(location, true, out var mouseUpStacked) is KeyframePoint clickedKeyframe &&
|
||||
mouseUpStacked)
|
||||
{
|
||||
SelectKeyframePoint(clickedKeyframe, false);
|
||||
}
|
||||
|
||||
_isMovingSelection = false;
|
||||
_isMovingTangent = false;
|
||||
@@ -473,22 +610,12 @@ namespace FlaxEditor.GUI
|
||||
if (!_movedView)
|
||||
{
|
||||
var selectionCount = _editor.SelectionCount;
|
||||
var point = GetChildAt(location) as KeyframePoint;
|
||||
if (selectionCount == 0 && point != null)
|
||||
var point = GetKeyframePointAt(location, false);
|
||||
if (point != null && (selectionCount == 0 || !point.IsSelected))
|
||||
{
|
||||
// Select node
|
||||
selectionCount = 1;
|
||||
point.IsSelected = true;
|
||||
if (_editor.ShowCollapsed)
|
||||
{
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var p = _editor._points[i];
|
||||
if (p.Index == point.Index)
|
||||
p.IsSelected = point.IsSelected;
|
||||
}
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
SelectKeyframePoint(point, false);
|
||||
}
|
||||
|
||||
var viewRect = _editor._mainPanel.GetClientArea();
|
||||
@@ -500,6 +627,16 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
cm.AddButton(selectionCount == 1 ? "Edit keyframe" : "Edit keyframes", () => _editor.EditKeyframes(this, location));
|
||||
}
|
||||
var components = _editor.Accessor.GetCurveComponents();
|
||||
if (point != null && !_editor.ShowCollapsed && components > 1)
|
||||
{
|
||||
var componentMenu = cm.AddChildMenu("Select component");
|
||||
for (int i = 0; i < components; i++)
|
||||
{
|
||||
var component = i;
|
||||
componentMenu.ContextMenu.AddButton(GetComponentLabel(component, typeof(T)), () => SelectKeyframeComponent(point.Index, component));
|
||||
}
|
||||
}
|
||||
var totalSelectionCount = _editor.KeyframesEditorContext?.OnKeyframesSelectionCount() ?? selectionCount;
|
||||
if (totalSelectionCount > 0)
|
||||
{
|
||||
@@ -558,7 +695,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Add keyframe on double click
|
||||
var child = GetChildAt(location);
|
||||
if (child is not KeyframePoint &&
|
||||
if (GetKeyframePointAt(location, false) == null &&
|
||||
child is not TangentPoint &&
|
||||
_editor.KeyframesCount < _editor.MaxKeyframes)
|
||||
{
|
||||
@@ -591,7 +728,10 @@ namespace FlaxEditor.GUI
|
||||
// Scale relative to the curve size
|
||||
var scale = new Float2(delta * 0.1f);
|
||||
_editor._mainPanel.GetDesireClientArea(out var mainPanelArea);
|
||||
var curveScale = mainPanelArea.Size / _editor._contents.Size;
|
||||
var contentsSize = _editor._contents.Size;
|
||||
var curveScale = new Float2(
|
||||
GetSafeZoomRatio(mainPanelArea.Width, contentsSize.X),
|
||||
GetSafeZoomRatio(mainPanelArea.Height, contentsSize.Y));
|
||||
scale *= curveScale;
|
||||
if (zoomAlt)
|
||||
scale.X = 0; // Scale Y axis only
|
||||
|
||||
@@ -63,17 +63,29 @@ namespace FlaxEditor.GUI
|
||||
|
||||
private class Popup : ContextMenuBase
|
||||
{
|
||||
private const float HeaderHeight = 12.0f;
|
||||
private const float MinContentHeight = 120.0f;
|
||||
|
||||
private CustomEditorPresenter _presenter;
|
||||
private CurveEditor<T> _editor;
|
||||
private List<int> _keyframeIndices;
|
||||
private Panel _panel;
|
||||
private bool _isDirty;
|
||||
private bool _isDragging;
|
||||
private Float2 _dragStartScreenPos;
|
||||
private Float2 _dragStartWindowPos;
|
||||
|
||||
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float maxHeight = 140.0f)
|
||||
: this(editor, maxHeight)
|
||||
{
|
||||
_presenter.Select(selection);
|
||||
_presenter.OpenAllGroups();
|
||||
Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight));
|
||||
var maxContentHeight = Mathf.Max(maxHeight - HeaderHeight, 1.0f);
|
||||
var desiredHeight = _presenter.ContainerControl.Size.Y;
|
||||
if (desiredHeight <= 1.0f)
|
||||
desiredHeight = maxContentHeight;
|
||||
Size = new Float2(Size.X, HeaderHeight + Mathf.Min(Mathf.Max(desiredHeight, MinContentHeight), maxContentHeight));
|
||||
UpdateContentBounds();
|
||||
_keyframeIndices = keyframeIndices;
|
||||
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
|
||||
throw new Exception();
|
||||
@@ -84,18 +96,39 @@ namespace FlaxEditor.GUI
|
||||
_editor = editor;
|
||||
const float width = 340.0f;
|
||||
Size = new Float2(width, height);
|
||||
var panel1 = new Panel(ScrollBars.Vertical)
|
||||
_panel = new Panel(ScrollBars.Vertical)
|
||||
{
|
||||
Bounds = new Rectangle(0, 0.0f, width, height),
|
||||
Bounds = new Rectangle(0, HeaderHeight, width, Mathf.Max(height - HeaderHeight, 1.0f)),
|
||||
Parent = this
|
||||
};
|
||||
_presenter = new CustomEditorPresenter(null);
|
||||
_presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
|
||||
_presenter.Panel.IsScrollable = true;
|
||||
_presenter.Panel.Parent = panel1;
|
||||
_presenter.Panel.Parent = _panel;
|
||||
_presenter.Modified += OnModified;
|
||||
}
|
||||
|
||||
private void UpdateContentBounds()
|
||||
{
|
||||
if (_panel != null)
|
||||
_panel.Bounds = new Rectangle(0, HeaderHeight, Width, Mathf.Max(Height - HeaderHeight, 1.0f));
|
||||
}
|
||||
|
||||
private bool IsOverHeader(ref Float2 location)
|
||||
{
|
||||
return location.X >= 0.0f && location.X <= Width && location.Y >= 0.0f && location.Y <= HeaderHeight;
|
||||
}
|
||||
|
||||
private void EndDragging()
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
_isDragging = false;
|
||||
Cursor = CursorType.Default;
|
||||
EndMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModified()
|
||||
{
|
||||
if (!_isDirty)
|
||||
@@ -108,7 +141,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
for (int i = 0; i < _presenter.SelectionCount; i++)
|
||||
{
|
||||
_editor.SetKeyframeInternal(_keyframeIndices[i], _presenter.Selection[i]);
|
||||
_editor.SetKeyframeInternal(_keyframeIndices[i], _editor.GetKeyframeFromEditingProxy(_presenter.Selection[i]));
|
||||
}
|
||||
}
|
||||
else if (_presenter.Selection[0] is IAllKeyframesProxy proxy)
|
||||
@@ -129,12 +162,31 @@ namespace FlaxEditor.GUI
|
||||
base.OnShow();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
var style = Style.Current;
|
||||
Render2D.FillRectangle(new Rectangle(0, 0, Width, HeaderHeight), style.BackgroundHighlighted);
|
||||
Render2D.FillRectangle(new Rectangle(0, HeaderHeight - 1.0f, Width, 1.0f), style.Background);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnSizeChanged()
|
||||
{
|
||||
base.OnSizeChanged();
|
||||
|
||||
UpdateContentBounds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Hide()
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
EndDragging();
|
||||
Focus(null);
|
||||
|
||||
if (_isDirty)
|
||||
@@ -145,13 +197,87 @@ namespace FlaxEditor.GUI
|
||||
|
||||
if (_editor._popup == this)
|
||||
_editor._popup = null;
|
||||
_presenter.Modified -= OnModified;
|
||||
_presenter = null;
|
||||
_editor = null;
|
||||
_keyframeIndices = null;
|
||||
_panel = null;
|
||||
|
||||
base.Hide();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && IsOverHeader(ref location))
|
||||
{
|
||||
_isDragging = true;
|
||||
_dragStartScreenPos = FlaxEngine.Input.MouseScreenPosition;
|
||||
_dragStartWindowPos = WindowLocation;
|
||||
Cursor = CursorType.SizeAll;
|
||||
StartMouseCapture();
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseMove(Float2 location)
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
var screenPos = FlaxEngine.Input.MouseScreenPosition;
|
||||
MoveWindowTo(_dragStartWindowPos + screenPos - _dragStartScreenPos);
|
||||
Cursor = CursorType.SizeAll;
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor = IsOverHeader(ref location) ? CursorType.SizeAll : CursorType.Default;
|
||||
|
||||
base.OnMouseMove(location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isDragging)
|
||||
{
|
||||
EndDragging();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseUp(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseLeave()
|
||||
{
|
||||
if (!_isDragging)
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
base.OnMouseLeave();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
EndDragging();
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
_isDragging = false;
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnKeyDown(KeyboardKeys key)
|
||||
{
|
||||
@@ -208,7 +334,8 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
var style = Style.Current;
|
||||
var rect = new Rectangle(Float2.Zero, Size);
|
||||
var color = Editor.ShowCollapsed ? style.ForegroundDisabled : Editor.Colors[Component];
|
||||
var axisColor = Editor.ShowCollapsed ? style.ForegroundDisabled : Editor.Colors[Component];
|
||||
var color = axisColor;
|
||||
if (IsSelected)
|
||||
color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f);
|
||||
if (IsMouseOver)
|
||||
@@ -288,22 +415,14 @@ namespace FlaxEditor.GUI
|
||||
set => Editor.SetKeyframeTangentInternal(Index, IsIn, Component, value);
|
||||
}
|
||||
|
||||
internal float TangentOffset => 50.0f / Editor.ViewScale.X;
|
||||
private const float TangentVisualOffset = 50.0f;
|
||||
|
||||
internal float TangentOffset => TangentVisualOffset / Editor.ViewScale.X;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
var style = Style.Current;
|
||||
var thickness = 6.0f / Mathf.Max(Editor.ViewScale.X, 1.0f);
|
||||
var size = Size;
|
||||
var pointPos = PointFromParent(Point.Center);
|
||||
Render2D.DrawLine(size * 0.5f, pointPos, style.ForegroundDisabled, thickness);
|
||||
|
||||
var rect = new Rectangle(Float2.Zero, size);
|
||||
var color = style.BorderSelected;
|
||||
if (IsMouseOver)
|
||||
color *= 1.1f;
|
||||
Render2D.FillRectangle(rect, color);
|
||||
// Drawn by the editor overlay to keep a constant screen-space size.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -436,12 +555,41 @@ namespace FlaxEditor.GUI
|
||||
/// </summary>
|
||||
protected readonly TangentPoint[] _tangents = new TangentPoint[2];
|
||||
|
||||
private static bool IsFinite(float value)
|
||||
{
|
||||
return !float.IsNaN(value) && !float.IsInfinity(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the input value if finite, otherwise returns a finite fallback or zero.
|
||||
/// </summary>
|
||||
protected static float SanitizeFinite(float value, float fallback)
|
||||
{
|
||||
if (IsFinite(value))
|
||||
return value;
|
||||
return IsFinite(fallback) ? fallback : 0.0f;
|
||||
}
|
||||
|
||||
private static float SanitizeViewScale(float value, float fallback)
|
||||
{
|
||||
if (!IsFinite(value))
|
||||
value = IsFinite(fallback) ? fallback : 1.0f;
|
||||
return Mathf.Clamp(value, 0.0001f, 1000.0f);
|
||||
}
|
||||
|
||||
private static float GetSafeZoomRatio(float viewSize, float contentsSize)
|
||||
{
|
||||
return IsFinite(viewSize) && IsFinite(contentsSize) && viewSize > Mathf.Epsilon && contentsSize > Mathf.Epsilon ? viewSize / contentsSize : 0.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Float2 ViewOffset
|
||||
{
|
||||
get => _mainPanel.ViewOffset;
|
||||
set
|
||||
{
|
||||
value.X = SanitizeFinite(value.X, _mainPanel.ViewOffset.X);
|
||||
value.Y = SanitizeFinite(value.Y, _mainPanel.ViewOffset.Y);
|
||||
_mainPanel.ViewOffset = value;
|
||||
_mainPanel.FastScroll();
|
||||
}
|
||||
@@ -451,7 +599,13 @@ namespace FlaxEditor.GUI
|
||||
public override Float2 ViewScale
|
||||
{
|
||||
get => _contents.Scale;
|
||||
set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(1000.0f));
|
||||
set
|
||||
{
|
||||
var scale = _contents.Scale;
|
||||
value.X = SanitizeViewScale(value.X, scale.X);
|
||||
value.Y = SanitizeViewScale(value.Y, scale.Y);
|
||||
_contents.Scale = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -705,6 +859,36 @@ namespace FlaxEditor.GUI
|
||||
/// <returns>The proxy object.</returns>
|
||||
protected abstract IAllKeyframesProxy GetAllKeyframesEditingProxy();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an editing proxy for a single keyframe.
|
||||
/// </summary>
|
||||
/// <param name="index">The keyframe index.</param>
|
||||
/// <param name="keyframe">The keyframe.</param>
|
||||
/// <returns>The keyframe editing proxy.</returns>
|
||||
protected virtual object CreateKeyframeEditingProxy(int index, object keyframe)
|
||||
{
|
||||
return keyframe;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets keyframe data from its editing proxy.
|
||||
/// </summary>
|
||||
/// <param name="proxy">The keyframe editing proxy.</param>
|
||||
/// <returns>The keyframe data.</returns>
|
||||
protected virtual object GetKeyframeFromEditingProxy(object proxy)
|
||||
{
|
||||
return proxy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum height for keyframe editing popup.
|
||||
/// </summary>
|
||||
/// <returns>The maximum popup height.</returns>
|
||||
protected virtual float GetKeyframeEditingPopupHeight()
|
||||
{
|
||||
return 320.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for keyframes editing proxy objects.
|
||||
/// </summary>
|
||||
@@ -740,8 +924,8 @@ namespace FlaxEditor.GUI
|
||||
var selection = new object[keyframeIndices.Count];
|
||||
var keyframes = GetKeyframes();
|
||||
for (int i = 0; i < keyframeIndices.Count; i++)
|
||||
selection[i] = keyframes[keyframeIndices[i]];
|
||||
_popup = new Popup(this, selection, keyframeIndices);
|
||||
selection[i] = CreateKeyframeEditingProxy(keyframeIndices[i], keyframes[keyframeIndices[i]]);
|
||||
_popup = new Popup(this, selection, keyframeIndices, GetKeyframeEditingPopupHeight());
|
||||
_popup.Show(control, pos);
|
||||
}
|
||||
|
||||
@@ -1049,6 +1233,58 @@ namespace FlaxEditor.GUI
|
||||
/// <param name="viewRect">The main panel client area used as a view bounds.</param>
|
||||
protected abstract void DrawCurve(ref Rectangle viewRect);
|
||||
|
||||
private Float2 ContentsToEditor(Float2 location)
|
||||
{
|
||||
location = _contents.PointToParent(location);
|
||||
return _mainPanel.PointToParent(location);
|
||||
}
|
||||
|
||||
private Float2 GetControlCenterInEditor(Control control)
|
||||
{
|
||||
var center = control.PointToParent(control.Size * 0.5f);
|
||||
return ContentsToEditor(center);
|
||||
}
|
||||
|
||||
private void DrawTangentHandles()
|
||||
{
|
||||
var style = Style.Current;
|
||||
for (int i = 0; i < _tangents.Length; i++)
|
||||
{
|
||||
var tangent = _tangents[i];
|
||||
if (!tangent.Visible || tangent.Point == null || !tangent.Point.Visible)
|
||||
continue;
|
||||
|
||||
var tangentCenter = GetControlCenterInEditor(tangent);
|
||||
var keyframeCenter = GetControlCenterInEditor(tangent.Point);
|
||||
Render2D.DrawLine(tangentCenter, keyframeCenter, style.ForegroundDisabled, 2.0f);
|
||||
|
||||
var rect = new Rectangle(tangentCenter - KeyframesSize * 0.5f, KeyframesSize);
|
||||
var color = style.BorderSelected;
|
||||
if (tangent.IsMouseOver)
|
||||
color *= 1.1f;
|
||||
Render2D.FillRectangle(rect, color);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSelectedKeyframeLabels()
|
||||
{
|
||||
if (ShowCollapsed)
|
||||
return;
|
||||
|
||||
var style = Style.Current;
|
||||
for (int i = 0; i < _points.Count; i++)
|
||||
{
|
||||
var point = _points[i];
|
||||
if (!point.Visible || !point.IsSelected)
|
||||
continue;
|
||||
|
||||
var center = GetControlCenterInEditor(point);
|
||||
var label = GetComponentLabel(point.Component, ValueType);
|
||||
var labelRect = new Rectangle(center.X + 12.0f, center.Y - 22.0f, 34.0f, 28.0f);
|
||||
Render2D.DrawText(style.FontMedium, label, labelRect, Colors[point.Component], TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 1.25f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
@@ -1097,14 +1333,18 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
var selectionRect = Rectangle.FromPoints
|
||||
(
|
||||
_mainPanel.PointToParent(_contents.PointToParent(_contents._leftMouseDownPos)),
|
||||
_mainPanel.PointToParent(_contents.PointToParent(_contents._mousePos))
|
||||
ContentsToEditor(_contents._leftMouseDownPos),
|
||||
ContentsToEditor(_contents._mousePos)
|
||||
);
|
||||
Render2D.FillRectangle(selectionRect, style.Selection);
|
||||
Render2D.DrawRectangle(selectionRect, style.SelectionBorder);
|
||||
}
|
||||
|
||||
base.Draw();
|
||||
Render2D.PushClip(ref viewRect);
|
||||
DrawTangentHandles();
|
||||
DrawSelectedKeyframeLabels();
|
||||
Render2D.PopClip();
|
||||
|
||||
// Draw border
|
||||
if (ContainsFocus)
|
||||
@@ -1465,6 +1705,85 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
sealed class KeyframeProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
public LinearCurveEditor<T> Editor;
|
||||
|
||||
[HideInEditor, NoSerialize]
|
||||
public int Index;
|
||||
|
||||
private float _time;
|
||||
private readonly float _originalTime;
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(0), VisibleIf(nameof(HasFPS))]
|
||||
[Tooltip("The keyframe frame number.")]
|
||||
public int Frame
|
||||
{
|
||||
get => HasFPS ? Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * Editor.FPS.Value) : 0;
|
||||
set
|
||||
{
|
||||
if (Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon)
|
||||
_time = value / Editor.FPS.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The time of the keyframe.")]
|
||||
public float Time
|
||||
{
|
||||
get => _time;
|
||||
set => _time = SanitizeFinite(value, _time);
|
||||
}
|
||||
|
||||
[EditorDisplay("Value"), EditorOrder(10), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The value of the curve at keyframe.")]
|
||||
public T Value;
|
||||
|
||||
private bool HasFPS => Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon;
|
||||
|
||||
public KeyframeProxy(LinearCurveEditor<T> editor, int index, LinearCurve<T>.Keyframe keyframe)
|
||||
{
|
||||
Editor = editor;
|
||||
Index = index;
|
||||
_time = _originalTime = keyframe.Time;
|
||||
Value = keyframe.Value;
|
||||
}
|
||||
|
||||
private float GetValidTime()
|
||||
{
|
||||
if (Editor == null)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
if (Editor.FPS.HasValue)
|
||||
{
|
||||
var fps = Editor.FPS.Value;
|
||||
if (fps <= Mathf.Epsilon)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
var frame = Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * fps);
|
||||
var minFrame = int.MinValue;
|
||||
var maxFrame = int.MaxValue;
|
||||
if (Index > 0)
|
||||
minFrame = Mathf.FloorToInt(Editor._keyframes[Index - 1].Time * fps) + 1;
|
||||
if (Index < Editor._keyframes.Count - 1)
|
||||
maxFrame = Mathf.FloorToInt(Editor._keyframes[Index + 1].Time * fps) - 1;
|
||||
if (minFrame > maxFrame)
|
||||
return _originalTime;
|
||||
frame = Mathf.Clamp(frame, minFrame, maxFrame);
|
||||
return frame / fps;
|
||||
}
|
||||
var minTime = Index > 0 ? Editor._keyframes[Index - 1].Time + Mathf.Epsilon : float.MinValue;
|
||||
var maxTime = Index < Editor._keyframes.Count - 1 ? Editor._keyframes[Index + 1].Time - Mathf.Epsilon : float.MaxValue;
|
||||
if (minTime > maxTime)
|
||||
return _originalTime;
|
||||
return Mathf.Clamp(SanitizeFinite(_time, _originalTime), minTime, maxTime);
|
||||
}
|
||||
|
||||
public LinearCurve<T>.Keyframe ToKeyframe()
|
||||
{
|
||||
return new LinearCurve<T>.Keyframe(GetValidTime(), Value);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AllKeyframesProxy : IAllKeyframesProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
@@ -1489,6 +1808,24 @@ namespace FlaxEditor.GUI
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object CreateKeyframeEditingProxy(int index, object keyframe)
|
||||
{
|
||||
return new KeyframeProxy(this, index, (LinearCurve<T>.Keyframe)keyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object GetKeyframeFromEditingProxy(object proxy)
|
||||
{
|
||||
return proxy is KeyframeProxy keyframeProxy ? keyframeProxy.ToKeyframe() : proxy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float GetKeyframeEditingPopupHeight()
|
||||
{
|
||||
return 240.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object[] GetKeyframes()
|
||||
{
|
||||
@@ -2213,6 +2550,95 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
sealed class KeyframeProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
public BezierCurveEditor<T> Editor;
|
||||
|
||||
[HideInEditor, NoSerialize]
|
||||
public int Index;
|
||||
|
||||
private float _time;
|
||||
private readonly float _originalTime;
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(0), VisibleIf(nameof(HasFPS))]
|
||||
[Tooltip("The keyframe frame number.")]
|
||||
public int Frame
|
||||
{
|
||||
get => HasFPS ? Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * Editor.FPS.Value) : 0;
|
||||
set
|
||||
{
|
||||
if (Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon)
|
||||
_time = value / Editor.FPS.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The time of the keyframe.")]
|
||||
public float Time
|
||||
{
|
||||
get => _time;
|
||||
set => _time = SanitizeFinite(value, _time);
|
||||
}
|
||||
|
||||
[EditorDisplay("Value"), EditorOrder(10), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The value of the curve at keyframe.")]
|
||||
public T Value;
|
||||
|
||||
[EditorDisplay("Tangents", "Tangent In"), EditorOrder(20), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The input tangent (going from the previous key to this one) of the key.")]
|
||||
public T TangentIn;
|
||||
|
||||
[EditorDisplay("Tangents", "Tangent Out"), EditorOrder(21), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The output tangent (going from this key to next one) of the key.")]
|
||||
public T TangentOut;
|
||||
|
||||
private bool HasFPS => Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon;
|
||||
|
||||
public KeyframeProxy(BezierCurveEditor<T> editor, int index, BezierCurve<T>.Keyframe keyframe)
|
||||
{
|
||||
Editor = editor;
|
||||
Index = index;
|
||||
_time = _originalTime = keyframe.Time;
|
||||
Value = keyframe.Value;
|
||||
TangentIn = keyframe.TangentIn;
|
||||
TangentOut = keyframe.TangentOut;
|
||||
}
|
||||
|
||||
private float GetValidTime()
|
||||
{
|
||||
if (Editor == null)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
if (Editor.FPS.HasValue)
|
||||
{
|
||||
var fps = Editor.FPS.Value;
|
||||
if (fps <= Mathf.Epsilon)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
var frame = Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * fps);
|
||||
var minFrame = int.MinValue;
|
||||
var maxFrame = int.MaxValue;
|
||||
if (Index > 0)
|
||||
minFrame = Mathf.FloorToInt(Editor._keyframes[Index - 1].Time * fps) + 1;
|
||||
if (Index < Editor._keyframes.Count - 1)
|
||||
maxFrame = Mathf.FloorToInt(Editor._keyframes[Index + 1].Time * fps) - 1;
|
||||
if (minFrame > maxFrame)
|
||||
return _originalTime;
|
||||
frame = Mathf.Clamp(frame, minFrame, maxFrame);
|
||||
return frame / fps;
|
||||
}
|
||||
var minTime = Index > 0 ? Editor._keyframes[Index - 1].Time + Mathf.Epsilon : float.MinValue;
|
||||
var maxTime = Index < Editor._keyframes.Count - 1 ? Editor._keyframes[Index + 1].Time - Mathf.Epsilon : float.MaxValue;
|
||||
if (minTime > maxTime)
|
||||
return _originalTime;
|
||||
return Mathf.Clamp(SanitizeFinite(_time, _originalTime), minTime, maxTime);
|
||||
}
|
||||
|
||||
public BezierCurve<T>.Keyframe ToKeyframe()
|
||||
{
|
||||
return new BezierCurve<T>.Keyframe(GetValidTime(), Value, TangentIn, TangentOut);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AllKeyframesProxy : IAllKeyframesProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
@@ -2237,6 +2663,24 @@ namespace FlaxEditor.GUI
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object CreateKeyframeEditingProxy(int index, object keyframe)
|
||||
{
|
||||
return new KeyframeProxy(this, index, (BezierCurve<T>.Keyframe)keyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object GetKeyframeFromEditingProxy(object proxy)
|
||||
{
|
||||
return proxy is KeyframeProxy keyframeProxy ? keyframeProxy.ToKeyframe() : proxy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float GetKeyframeEditingPopupHeight()
|
||||
{
|
||||
return 320.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object[] GetKeyframes()
|
||||
{
|
||||
|
||||
@@ -36,6 +36,11 @@ namespace FlaxEditor.GUI.Timeline.GUI
|
||||
_timeline.OnKeyframesSelection(null, this, selectionRect);
|
||||
}
|
||||
|
||||
private bool HasSelectionModifier()
|
||||
{
|
||||
return Root.GetKey(KeyboardKeys.Shift) || Root.GetKey(KeyboardKeys.Alt);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
@@ -48,7 +53,8 @@ namespace FlaxEditor.GUI.Timeline.GUI
|
||||
// Start selecting
|
||||
_isSelecting = true;
|
||||
_selectingStartPos = location;
|
||||
_timeline.OnKeyframesDeselect(null);
|
||||
if (!HasSelectionModifier())
|
||||
_timeline.OnKeyframesDeselect(null);
|
||||
Focus();
|
||||
StartMouseCapture();
|
||||
return true;
|
||||
|
||||
@@ -77,6 +77,13 @@ namespace FlaxEditor.GUI
|
||||
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
||||
private class Contents : ContainerControl
|
||||
{
|
||||
private enum SelectionMode
|
||||
{
|
||||
Replace,
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
private readonly KeyframesEditor _editor;
|
||||
internal bool _leftMouseDown;
|
||||
private bool _rightMouseDown;
|
||||
@@ -108,14 +115,39 @@ namespace FlaxEditor.GUI
|
||||
UpdateSelection(ref selectionRect);
|
||||
}
|
||||
|
||||
private SelectionMode GetSelectionMode()
|
||||
{
|
||||
if (Root.GetKey(KeyboardKeys.Alt))
|
||||
return SelectionMode.Remove;
|
||||
if (Root.GetKey(KeyboardKeys.Shift))
|
||||
return SelectionMode.Add;
|
||||
return SelectionMode.Replace;
|
||||
}
|
||||
|
||||
internal void UpdateSelection(ref Rectangle selectionRect)
|
||||
{
|
||||
var mode = GetSelectionMode();
|
||||
|
||||
// Find controls to select
|
||||
for (int i = 0; i < Children.Count; i++)
|
||||
{
|
||||
if (Children[i] is KeyframePoint p)
|
||||
{
|
||||
p.IsSelected = p.Bounds.Intersects(ref selectionRect);
|
||||
var intersects = p.Bounds.Intersects(ref selectionRect);
|
||||
switch (mode)
|
||||
{
|
||||
case SelectionMode.Replace:
|
||||
p.IsSelected = intersects;
|
||||
break;
|
||||
case SelectionMode.Add:
|
||||
if (intersects)
|
||||
p.IsSelected = true;
|
||||
break;
|
||||
case SelectionMode.Remove:
|
||||
if (intersects)
|
||||
p.IsSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +170,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
var viewRect = _editor._mainPanel.GetClientArea();
|
||||
var locationKeyframes = PointToKeyframes(location, ref viewRect);
|
||||
var moved = false;
|
||||
for (var i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var p = _editor._points[i];
|
||||
@@ -160,7 +193,12 @@ namespace FlaxEditor.GUI
|
||||
// TODO: snapping keyframes to grid when moving
|
||||
|
||||
_editor._keyframes[p.Index] = k;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (moved)
|
||||
{
|
||||
_editor.UpdateKeyframes();
|
||||
if (_editor.EnablePanning)
|
||||
{
|
||||
@@ -351,10 +389,13 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Start selecting
|
||||
StartMouseCapture();
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
if (GetSelectionMode() == SelectionMode.Replace)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
}
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.GUI.Timeline.Undo;
|
||||
using FlaxEditor.SceneGraph;
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
|
||||
|
||||
private void OnSplitterMoved(Float2 location)
|
||||
{
|
||||
var height = Mathf.Clamp(PointToParent(location).Y, 40.0f, 1000.0f);
|
||||
var height = Mathf.Clamp(_splitter.PointToParent(location).Y + Height - Curve.Height, 40.0f, 1000.0f);
|
||||
if (!Mathf.NearEqual(height, _expandedHeight))
|
||||
{
|
||||
Height = _expandedHeight = height;
|
||||
|
||||
@@ -100,6 +100,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
|
||||
/// <param name="obj">The object.</param>
|
||||
protected virtual void OnObjectExistenceChanged(object obj)
|
||||
{
|
||||
TooltipText = obj != null ? Editor.Instance.CodeDocs.GetTooltip(obj.GetType()) : "null";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -829,6 +829,7 @@ namespace FlaxEditor.Modules
|
||||
tooltip.Append($" ({engineNickname})");
|
||||
|
||||
// Build info
|
||||
tooltip.AppendLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
|
||||
#if USE_LARGE_WORLDS
|
||||
tooltip.AppendLine("Large Worlds Enabled");
|
||||
#endif
|
||||
|
||||
@@ -60,14 +60,14 @@ namespace FlaxEditor.Options
|
||||
public sealed class InputOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value based on the current settings that indicates wether window shortcuts will be avaliable during play mode.
|
||||
/// Gets a value based on the current settings that indicates whether window shortcuts will be available during play mode.
|
||||
/// </summary>
|
||||
public static bool WindowShortcutsAvaliable => !Editor.IsPlayMode || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.All;
|
||||
public static bool WindowShortcutsAvailable => !Editor.IsPlayMode || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.All;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value based on the current settings that indicates wether the profiler window shortcut will be avaliable during play mode.
|
||||
/// Gets a value based on the current settings that indicates whether the profiler window shortcut will be available during play mode.
|
||||
/// </summary>
|
||||
public static bool ProfilerShortcutAvaliable => WindowShortcutsAvaliable || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.ProfilerOnly;
|
||||
public static bool ProfilerShortcutAvailable => WindowShortcutsAvailable || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.ProfilerOnly;
|
||||
|
||||
#region Common
|
||||
|
||||
|
||||
@@ -106,28 +106,28 @@ namespace FlaxEditor.Options
|
||||
/// Gets or sets the output log text color.
|
||||
/// </summary>
|
||||
[DefaultValue(typeof(Color), "1,1,1,1")]
|
||||
[EditorDisplay("Log", "Info Color"), EditorOrder(1500), Tooltip("The color used for info messages in the Debug and Output Log.")]
|
||||
[EditorDisplay("Debug Log", "Info Color"), EditorOrder(1500), Tooltip("The color used for info messages in the Debug and Output Log.")]
|
||||
public Color LogInfoColor { get; set; } = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output log text color for warnings
|
||||
/// </summary>
|
||||
[DefaultValue(typeof(Color), "1,1,0,1")]
|
||||
[EditorDisplay("Log", "Warning Color"), EditorOrder(1501), Tooltip("The color used for warnings in the Debug and Output Log.")]
|
||||
[EditorDisplay("Debug Log", "Warning Color"), EditorOrder(1501), Tooltip("The color used for warnings in the Debug and Output Log.")]
|
||||
public Color LogWarningColor { get; set; } = Color.Yellow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output log text color for errors
|
||||
/// </summary>
|
||||
[DefaultValue(typeof(Color), "1,0,0,1")]
|
||||
[EditorDisplay("Log", "Error Color"), EditorOrder(1502), Tooltip("The color used for errors in the Debug and Output Log.")]
|
||||
[EditorDisplay("Debug Log", "Error Color"), EditorOrder(1502), Tooltip("The color used for errors in the Debug and Output Log.")]
|
||||
public Color LogErrorColor { get; set; } = Color.Red;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value wether the Debug Log entry text color should use the set color.
|
||||
/// Gets or sets a value whether the Debug Log entry text color should use the set color.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
[EditorDisplay("Log", "Color Debug Log Text"), EditorOrder(1503), Tooltip("Wether to use the set colors in the text of a Debug Log entry.")]
|
||||
[EditorDisplay("Debug Log", "Color Debug Log Text"), EditorOrder(1503), Tooltip("Wether to use the set colors in the text of a Debug Log entry.")]
|
||||
public bool ColorDebugLogText = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ namespace FlaxEditor.SceneGraph
|
||||
// Skip actors that should not be selected
|
||||
if (hit != null && _actor != null && (_actor.HideFlags & HideFlags.DontSelect) == HideFlags.DontSelect)
|
||||
{
|
||||
hit = parentNode;
|
||||
hit = null;
|
||||
}
|
||||
|
||||
return hit;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Real = System.Single;
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.SceneGraph.Actors
|
||||
@@ -24,6 +25,67 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
/// <seealso cref="FlaxEditor.SceneGraph.ActorChildNode{T}" />
|
||||
public sealed class SideLinkNode : ActorChildNode<BoxBrushNode>
|
||||
{
|
||||
private const float MinimumComponentExtent = 0.001f;
|
||||
|
||||
internal static Vector3 GetCornerSigns(int cornerIndex)
|
||||
{
|
||||
switch (cornerIndex)
|
||||
{
|
||||
case 0: return new Vector3(1, 1, 1);
|
||||
case 1: return new Vector3(1, 1, -1);
|
||||
case 2: return new Vector3(-1, 1, -1);
|
||||
case 3: return new Vector3(-1, 1, 1);
|
||||
case 4: return new Vector3(1, -1, 1);
|
||||
case 5: return new Vector3(1, -1, -1);
|
||||
case 6: return new Vector3(-1, -1, -1);
|
||||
default: return new Vector3(-1, -1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Vector3 GetComponentPoint(BoxBrush brush, Vector3 signs)
|
||||
{
|
||||
return brush.Transform.LocalToWorld(brush.Center + signs * brush.Size * 0.5f);
|
||||
}
|
||||
|
||||
internal static void SetComponentPoint(BoxBrush brush, Vector3 signs, Vector3 worldPoint)
|
||||
{
|
||||
var point = brush.Transform.WorldToLocal(worldPoint);
|
||||
var center = brush.Center;
|
||||
var size = brush.Size;
|
||||
for (int axis = 0; axis < 3; axis++)
|
||||
{
|
||||
float sign = GetComponent(signs, axis);
|
||||
if (Mathf.Abs(sign) < 0.5f)
|
||||
continue;
|
||||
float halfSize = GetComponent(size, axis) * 0.5f;
|
||||
float opposite = GetComponent(center, axis) - sign * halfSize;
|
||||
float target = GetComponent(point, axis);
|
||||
if (sign > 0.0f)
|
||||
target = Mathf.Max(target, opposite + MinimumComponentExtent);
|
||||
else
|
||||
target = Mathf.Min(target, opposite - MinimumComponentExtent);
|
||||
SetComponent(ref center, axis, (target + opposite) * 0.5f);
|
||||
SetComponent(ref size, axis, Mathf.Abs(target - opposite));
|
||||
}
|
||||
brush.Center = center;
|
||||
brush.Size = size;
|
||||
}
|
||||
|
||||
private static float GetComponent(Vector3 value, int axis)
|
||||
{
|
||||
return axis == 0 ? (float)value.X : axis == 1 ? (float)value.Y : (float)value.Z;
|
||||
}
|
||||
|
||||
private static void SetComponent(ref Vector3 value, int axis, float component)
|
||||
{
|
||||
if (axis == 0)
|
||||
value.X = component;
|
||||
else if (axis == 1)
|
||||
value.Y = component;
|
||||
else
|
||||
value.Z = component;
|
||||
}
|
||||
|
||||
private sealed class BrushSurfaceProxy
|
||||
{
|
||||
[HideInEditor]
|
||||
@@ -45,6 +107,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(typeof(Float2), "1,1")]
|
||||
[EditorOrder(30), EditorDisplay("Brush", "UV Scale"), Limit(-1000, 1000, 0.01f)]
|
||||
[Tooltip("The surface texture coordinates scale.")]
|
||||
public Float2 TexCoordScale
|
||||
@@ -58,6 +121,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(typeof(Float2), "0,0")]
|
||||
[EditorOrder(40), EditorDisplay("Brush", "UV Offset"), Limit(-1000, 1000, 0.01f)]
|
||||
[Tooltip("The surface texture coordinates offset.")]
|
||||
public Float2 TexCoordOffset
|
||||
@@ -71,6 +135,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(0.0f)]
|
||||
[EditorOrder(50), EditorDisplay("Brush", "UV Rotation")]
|
||||
[Tooltip("The surface texture coordinates rotation angle (in degrees).")]
|
||||
public float TexCoordRotation
|
||||
@@ -84,6 +149,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(1.0f)]
|
||||
[EditorOrder(20), EditorDisplay("Brush", "Scale In Lightmap"), Limit(0, 10000, 0.1f)]
|
||||
[Tooltip("The scale in lightmap (per surface).")]
|
||||
public float ScaleInLightmap
|
||||
@@ -164,6 +230,9 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
set
|
||||
{
|
||||
var actor = Brush;
|
||||
#if true
|
||||
SetComponentPoint(actor, _offset * 2.0f, value.Translation);
|
||||
#else
|
||||
Transform localTrans = actor.Transform.WorldToLocal(value);
|
||||
var prevLocalOffset = _offset * actor.Size + actor.Center;
|
||||
var localOffset = Vector3.Abs(_offset) * 2.0f * localTrans.Translation;
|
||||
@@ -171,6 +240,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
float centerScale = Index % 2 == 0 ? 0.5f : -0.5f;
|
||||
actor.Size += localOffsetDelta;
|
||||
actor.Center += localOffsetDelta * centerScale;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
Parent = this
|
||||
};
|
||||
_toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window.");
|
||||
_toolstrip.AddButton(editor.Icons.Search64, ShowInContent).LinkTooltip("Show and select in Content Window.");
|
||||
|
||||
InputActions.Add(options => options.Save, Save);
|
||||
|
||||
@@ -132,12 +132,29 @@ namespace FlaxEditor.Windows.Assets
|
||||
// Cancel closing
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discard and close
|
||||
DiscardChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnClosing(reason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discards unsaved asset changes before closing the window.
|
||||
/// </summary>
|
||||
protected virtual void DiscardChanges()
|
||||
{
|
||||
}
|
||||
|
||||
private void ShowInContent()
|
||||
{
|
||||
Editor.Windows.ContentWin.Select(_item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
@@ -166,7 +183,9 @@ namespace FlaxEditor.Windows.Assets
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Called when scripts reloading begins.
|
||||
/// </summary>
|
||||
protected virtual void OnScriptsReloadBegin()
|
||||
{
|
||||
if (!IsHidden)
|
||||
@@ -257,7 +276,8 @@ namespace FlaxEditor.Windows.Assets
|
||||
base.OnShowContextMenu(menu);
|
||||
|
||||
menu.AddButton("Save", Save).Enabled = IsEdited;
|
||||
menu.AddButton("Copy name", () => Clipboard.Text = Item.NamePath);
|
||||
menu.AddButton("Copy Name", () => Clipboard.Text = Item.NamePath);
|
||||
menu.AddButton("Show In Content", ShowInContent);
|
||||
|
||||
menu.AddSeparator();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ namespace FlaxEditor.Windows.Assets
|
||||
[EditorOrder(5), EditorDisplay("Properties"), Tooltip("The rasterization mode used when generating font atlases.")]
|
||||
public FontRasterMode RasterMode;
|
||||
|
||||
[DefaultValue(32.0f), VisibleIf(nameof(IsMSDF))]
|
||||
[EditorOrder(6), Limit(4, 512), EditorDisplay("Properties"), Tooltip("The font size used when generating MSDF font atlases.")]
|
||||
public float MSDFSize;
|
||||
|
||||
[DefaultValue(FontHinting.Default)]
|
||||
[EditorOrder(10), EditorDisplay("Properties"), Tooltip("The font hinting used when rendering characters.")]
|
||||
public FontHinting Hinting;
|
||||
@@ -41,12 +45,15 @@ namespace FlaxEditor.Windows.Assets
|
||||
[EditorOrder(40), EditorDisplay("Properties"), Tooltip("Enables slant effect, emulating italic style.")]
|
||||
public bool Italic;
|
||||
|
||||
private bool IsMSDF => RasterMode == FontRasterMode.MSDF;
|
||||
|
||||
public void Get(out FontOptions options)
|
||||
{
|
||||
options = new FontOptions
|
||||
{
|
||||
Hinting = Hinting,
|
||||
RasterMode = RasterMode,
|
||||
MSDFSize = MSDFSize,
|
||||
};
|
||||
if (AntiAliasing)
|
||||
options.Flags |= FontFlags.AntiAliasing;
|
||||
@@ -63,6 +70,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
Bold = (options.Flags & FontFlags.Bold) == FontFlags.Bold;
|
||||
Italic = (options.Flags & FontFlags.Italic) == FontFlags.Italic;
|
||||
RasterMode = options.RasterMode;
|
||||
MSDFSize = options.MSDFSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +142,6 @@ namespace FlaxEditor.Windows.Assets
|
||||
if (assetOptions != options)
|
||||
{
|
||||
Asset.Options = options;
|
||||
Asset.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,15 @@ namespace FlaxEditor.Windows.Assets
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DiscardChanges()
|
||||
{
|
||||
Asset?.ClearInstance();
|
||||
_object = null;
|
||||
|
||||
base.DiscardChanges();
|
||||
}
|
||||
|
||||
private void OpenOptionsContextMenu()
|
||||
{
|
||||
if (_optionsCM != null)
|
||||
|
||||
@@ -64,8 +64,14 @@ namespace FlaxEditor.Windows.Assets
|
||||
[CustomEditor(typeof(ParametersEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private struct ParameterState
|
||||
{
|
||||
public object Value;
|
||||
public bool IsOverride;
|
||||
}
|
||||
|
||||
private MaterialBase _restoreBase;
|
||||
private Dictionary<string, object> _restoreParams;
|
||||
private Dictionary<string, ParameterState> _restoreParams;
|
||||
|
||||
[EditorDisplay("General"), Tooltip("The base material used to override it's properties")]
|
||||
public MaterialBase BaseMaterial
|
||||
@@ -181,9 +187,16 @@ namespace FlaxEditor.Windows.Assets
|
||||
var material = Window.Asset;
|
||||
_restoreBase = material.BaseMaterial;
|
||||
var parameters = material.Parameters;
|
||||
_restoreParams = new Dictionary<string, object>();
|
||||
_restoreParams = new Dictionary<string, ParameterState>();
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
_restoreParams[parameters[i].Name] = parameters[i].Value;
|
||||
{
|
||||
var p = parameters[i];
|
||||
_restoreParams[p.Name] = new ParameterState
|
||||
{
|
||||
Value = p.Value,
|
||||
IsOverride = p.IsOverride,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -191,7 +204,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
if (Window == null)
|
||||
if (Window == null || _restoreParams == null)
|
||||
return;
|
||||
|
||||
var material = Window.Asset;
|
||||
@@ -200,9 +213,10 @@ namespace FlaxEditor.Windows.Assets
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var p = parameters[i];
|
||||
if (p.IsPublic && _restoreParams.TryGetValue(p.Name, out var value))
|
||||
if (p.IsPublic && _restoreParams.TryGetValue(p.Name, out var state))
|
||||
{
|
||||
p.Value = value;
|
||||
p.Value = state.Value;
|
||||
p.IsOverride = state.IsOverride;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,6 +837,14 @@ namespace FlaxEditor.Windows.Assets
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DiscardChanges()
|
||||
{
|
||||
_asset?.Reload();
|
||||
|
||||
base.DiscardChanges();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
|
||||
@@ -755,6 +755,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
}
|
||||
_previewPlayerPicker.Parent.Visible = !_previewButton.Checked;
|
||||
_timeline.CanPlayPause = _previewButton.Checked || Editor.IsPlayMode;
|
||||
_timeline.CanPlayStop = _previewButton.Checked || Editor.IsPlayMode;
|
||||
}
|
||||
|
||||
private void OnRenderButtonClicked()
|
||||
@@ -927,7 +928,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
|
||||
UpdateToolstrip();
|
||||
_timeline.CanPlayPause = _previewButton.Checked;
|
||||
_timeline.CanPlayStop = false;
|
||||
_timeline.CanPlayStop = _previewButton.Checked;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -982,7 +983,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
// Preview is playing
|
||||
_previewPlayer.Tick(Time.UnscaledDeltaTime);
|
||||
}
|
||||
else if (Mathf.NearEqual(_previewPlayer.Time, _timeline.CurrentFrame))
|
||||
else if (Mathf.NearEqual(_previewPlayer.Time, _timeline.CurrentTime))
|
||||
{
|
||||
// Preview is paused
|
||||
_previewPlayer.Time = _timeline.CurrentTime;
|
||||
|
||||
@@ -77,11 +77,14 @@ namespace FlaxEditor.Windows
|
||||
public const float DefaultHeight = 32.0f;
|
||||
|
||||
private DebugLogWindow _window;
|
||||
private RichTextBox _richTextBox;
|
||||
public LogGroup Group;
|
||||
public LogEntryDescription Desc;
|
||||
public SpriteHandle Icon;
|
||||
public int LogCount = 1;
|
||||
|
||||
private Color Color => Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo);
|
||||
|
||||
public LogEntry(DebugLogWindow window, ref LogEntryDescription desc)
|
||||
: base(0, 0, 120, DefaultHeight)
|
||||
{
|
||||
@@ -106,6 +109,24 @@ namespace FlaxEditor.Windows
|
||||
Icon = _window._iconError;
|
||||
break;
|
||||
}
|
||||
|
||||
// Use Rich Text Box to display title if it contains any HTML tags
|
||||
if (desc.Title.Contains('<') && desc.Title.Contains('>'))
|
||||
{
|
||||
_richTextBox = new RichTextBox
|
||||
{
|
||||
ClipText = false,
|
||||
HasBorder = false,
|
||||
BackgroundColor = Color.Transparent,
|
||||
Text = desc.Title,
|
||||
};
|
||||
if (_window._colorDebugLogText)
|
||||
{
|
||||
var style = _richTextBox.TextStyle;
|
||||
style.Color = Color;
|
||||
_richTextBox.TextStyle = style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -116,12 +137,10 @@ namespace FlaxEditor.Windows
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
// Cache data
|
||||
var style = Style.Current;
|
||||
var index = IndexInParent;
|
||||
var clientRect = new Rectangle(Float2.Zero, Size);
|
||||
var color = Color;
|
||||
|
||||
// Background
|
||||
if (_window._selected == this)
|
||||
@@ -136,8 +155,6 @@ namespace FlaxEditor.Windows
|
||||
else if (index % 2 == 0)
|
||||
Render2D.FillRectangle(clientRect, style.Background * 0.9f);
|
||||
|
||||
var color = Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo);
|
||||
|
||||
// Icon
|
||||
Render2D.DrawSprite(Icon, new Rectangle(8, 0, 32, 32), color);
|
||||
|
||||
@@ -145,14 +162,37 @@ namespace FlaxEditor.Windows
|
||||
var textRect = new Rectangle(43, 2, clientRect.Width - 40, clientRect.Height - 10);
|
||||
Render2D.PushClip(ref clientRect);
|
||||
bool coloredText = _window._colorDebugLogText;
|
||||
if (LogCount == 1)
|
||||
if (_richTextBox != null)
|
||||
{
|
||||
Render2D.PushTransform(Matrix3x3.Translation2D(textRect.Location));
|
||||
_richTextBox.DrawSelf();
|
||||
Render2D.PopTransform();
|
||||
}
|
||||
else
|
||||
{
|
||||
Render2D.DrawText(style.FontMedium, Desc.Title, textRect, coloredText ? color : style.Foreground);
|
||||
}
|
||||
else if (LogCount > 1)
|
||||
|
||||
// Adding log counter for collapsed logs
|
||||
if (LogCount > 1)
|
||||
{
|
||||
Render2D.DrawText(style.FontMedium, $"{Desc.Title} ({LogCount})", textRect, coloredText ? color : style.Foreground);
|
||||
Float2 logCountPos = Float2.Zero;
|
||||
if (_richTextBox != null)
|
||||
{
|
||||
var blocks = _richTextBox.TextBlocks;
|
||||
if (blocks.Count != 0)
|
||||
{
|
||||
var block = blocks[^1];
|
||||
logCountPos = new Float2(block.Bounds.Right, block.Bounds.Top);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logCountPos.X = style.FontMedium.MeasureText(Desc.Title).X;
|
||||
}
|
||||
Render2D.DrawText(style.FontMedium, $" ({LogCount})", color, textRect.Location + logCountPos);
|
||||
}
|
||||
|
||||
Render2D.PopClip();
|
||||
}
|
||||
|
||||
@@ -296,6 +336,18 @@ namespace FlaxEditor.Windows
|
||||
|
||||
base.OnMouseLeave();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_richTextBox != null)
|
||||
{
|
||||
_richTextBox.OnDestroy();
|
||||
_richTextBox = null;
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
|
||||
@@ -53,72 +53,72 @@ namespace FlaxEditor.Windows
|
||||
// Set up editor window shortcuts
|
||||
InputActions.Add(options => options.ContentWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.ContentWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.SceneWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.SceneWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.ToolboxWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.ToolboxWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.PropertiesWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.PropertiesWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.GameWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.GameWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.EditorWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.EditWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.DebugLogWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.DebugLogWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.OutputLogWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.OutputLogWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.GraphicsQualityWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.GraphicsQualityWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.GameCookerWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.GameCookerWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.ProfilerWindow, () =>
|
||||
{
|
||||
if (InputOptions.ProfilerShortcutAvaliable)
|
||||
if (InputOptions.ProfilerShortcutAvailable)
|
||||
Editor.Windows.ProfilerWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.ContentFinder, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.ContentFinding.ShowSearch();
|
||||
});
|
||||
InputActions.Add(options => options.VisualScriptDebuggerWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.VisualScriptDebuggerWin.FocusOrShow();
|
||||
});
|
||||
InputActions.Add(options => options.EditorOptionsWindow, () =>
|
||||
{
|
||||
if (InputOptions.WindowShortcutsAvaliable)
|
||||
if (InputOptions.WindowShortcutsAvailable)
|
||||
Editor.Windows.EditorOptionsWin.FocusOrShow();
|
||||
});
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ void RawData::AddSurface(Brush* brush, int32 brushSurfaceIndex, const Guid& surf
|
||||
slot->AddSurface(scaleInLightmap, lightmapUVsBox, firstVertex, vertexCount);
|
||||
|
||||
// Add surface to brush
|
||||
if (!brush)
|
||||
return;
|
||||
auto& brushData = Brushes[brush->GetBrushID()];
|
||||
if (brushData.Surfaces.Count() != brush->GetSurfacesCount())
|
||||
brushData.Surfaces.Resize(brush->GetSurfacesCount());
|
||||
|
||||
@@ -93,5 +93,15 @@ namespace FlaxEngine
|
||||
string str = instance != null ? JsonSerializer.Serialize(instance) : null;
|
||||
Data = str;
|
||||
}
|
||||
|
||||
#if FLAX_EDITOR
|
||||
/// <summary>
|
||||
/// Clears the cached managed instance. The next <see cref="Instance"/> access will recreate it from the asset data.
|
||||
/// </summary>
|
||||
internal void ClearInstance()
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ public:
|
||||
const Upgrader upgraders[] =
|
||||
{
|
||||
{ 3, 4, &Upgrade_3_To_4 },
|
||||
{ 4, 5, &Upgrade_4_To_5 },
|
||||
};
|
||||
setup(upgraders, ARRAY_COUNT(upgraders));
|
||||
}
|
||||
|
||||
private:
|
||||
struct FontOptionsOld
|
||||
struct FontOptions3
|
||||
{
|
||||
FontHinting Hinting;
|
||||
FontFlags Flags;
|
||||
@@ -34,8 +35,8 @@ private:
|
||||
{
|
||||
ASSERT(context.Input.SerializedVersion == 3 && context.Output.SerializedVersion == 4);
|
||||
|
||||
FontOptionsOld optionsOld;
|
||||
Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptionsOld));
|
||||
FontOptions3 optionsOld;
|
||||
Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptions3));
|
||||
|
||||
FontOptions options;
|
||||
options.Hinting = optionsOld.Hinting;
|
||||
@@ -45,6 +46,30 @@ private:
|
||||
|
||||
return CopyChunk(context, 0);
|
||||
}
|
||||
|
||||
struct FontOptions4
|
||||
{
|
||||
FontHinting Hinting;
|
||||
FontFlags Flags;
|
||||
FontRasterMode RasterMode;
|
||||
};
|
||||
|
||||
static bool Upgrade_4_To_5(AssetMigrationContext& context)
|
||||
{
|
||||
ASSERT(context.Input.SerializedVersion == 4 && context.Output.SerializedVersion == 5);
|
||||
|
||||
FontOptions4 optionsOld;
|
||||
Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptions4));
|
||||
|
||||
FontOptions options;
|
||||
options.Hinting = optionsOld.Hinting;
|
||||
options.Flags = optionsOld.Flags;
|
||||
options.RasterMode = optionsOld.RasterMode;
|
||||
options.MSDFSize = 32.0f;
|
||||
context.Output.CustomData.Copy(&options);
|
||||
|
||||
return CopyChunk(context, 0);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
CreateAssetResult ImportFont::Import(CreateAssetContext& context)
|
||||
{
|
||||
// Base
|
||||
IMPORT_SETUP(FontAsset, 4);
|
||||
IMPORT_SETUP(FontAsset, 5);
|
||||
|
||||
// Setup header
|
||||
FontOptions options;
|
||||
options.Hinting = FontHinting::Default;
|
||||
options.Flags = FontFlags::AntiAliasing;
|
||||
options.RasterMode = FontRasterMode::Bitmap;
|
||||
options.MSDFSize = 32.0f;
|
||||
context.Data.CustomData.Copy(&options);
|
||||
|
||||
// Open the file
|
||||
|
||||
@@ -112,6 +112,16 @@ namespace FlaxEngine
|
||||
/// </summary>
|
||||
public float ValuesSum => R + G + B + A;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if color is fully transparent (all components are equal zero).
|
||||
/// </summary>
|
||||
public bool IsTransparent => Mathf.IsZero(R + G + B + A);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if color has opacity channel in use (different from 1).
|
||||
/// </summary>
|
||||
public bool HasOpacity => !Mathf.IsOne(A);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new Color with given r,g,b,a component.
|
||||
/// </summary>
|
||||
|
||||
+14
-14
@@ -783,12 +783,12 @@ public:
|
||||
/// <summary>
|
||||
/// Draws debug shapes for the actor and all child scripts.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnDebugDraw();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnDebugDraw();
|
||||
|
||||
/// <summary>
|
||||
/// Draws debug shapes for the selected actor and all child scripts.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnDebugDrawSelected();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnDebugDrawSelected();
|
||||
#endif
|
||||
|
||||
public:
|
||||
@@ -1054,64 +1054,64 @@ public:
|
||||
/// Serializes the actor object to the Json string. Serialized are only this actor properties but no child actors nor scripts. Serializes references to the other objects in a proper way using IDs.
|
||||
/// </summary>
|
||||
/// <returns>The Json container with serialized actor data.</returns>
|
||||
API_FUNCTION() String ToJson();
|
||||
API_FUNCTION(Attributes="NoAnimate") String ToJson();
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the actor object to the Json string. Deserialized are only this actor properties but no child actors nor scripts.
|
||||
/// </summary>
|
||||
/// <param name="json">The serialized actor data (state).</param>
|
||||
API_FUNCTION() void FromJson(const StringAnsiView& json);
|
||||
API_FUNCTION(Attributes="NoAnimate") void FromJson(const StringAnsiView& json);
|
||||
|
||||
/// <summary>
|
||||
/// Clones actor including all scripts and any child actors (whole scene tree). Objects are duplicated via serialization (any transient/non-saved state is ignored).
|
||||
/// </summary>
|
||||
API_FUNCTION() Actor* Clone();
|
||||
API_FUNCTION(Attributes="NoAnimate") Actor* Clone();
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Called when actor gets added to game systems. Occurs on BeginPlay event or when actor gets activated in hierarchy. Use this event to register object to other game system (eg. audio).
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnEnable();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnEnable();
|
||||
|
||||
/// <summary>
|
||||
/// Called when actor gets removed from game systems. Occurs on EndPlay event or when actor gets inactivated in hierarchy. Use this event to unregister object from other game system (eg. audio).
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnDisable();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnDisable();
|
||||
|
||||
/// <summary>
|
||||
/// Called when actor parent gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnParentChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnParentChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when actor transform gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnTransformChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnTransformChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when actor active state gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnActiveChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnActiveChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when actor active in tree state gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnActiveInTreeChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnActiveInTreeChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when order in parent children array gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnOrderInParentChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnOrderInParentChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when actor static flag gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnStaticFlagsChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnStaticFlagsChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when layer gets changed.
|
||||
/// </summary>
|
||||
API_FUNCTION() virtual void OnLayerChanged();
|
||||
API_FUNCTION(Attributes="NoAnimate") virtual void OnLayerChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Called when adding object to the game.
|
||||
|
||||
@@ -50,7 +50,18 @@ Array<BrushSurface> BoxBrush::GetSurfaces() const
|
||||
void BoxBrush::SetSurfaces(const Array<BrushSurface>& value)
|
||||
{
|
||||
CHECK(value.Count() == ARRAY_COUNT(Surfaces));
|
||||
Platform::MemoryCopy(Surfaces, value.Get(), sizeof(Surfaces));
|
||||
for (int32 i = 0; i < ARRAY_COUNT(Surfaces); i++)
|
||||
{
|
||||
auto& dst = Surfaces[i];
|
||||
const auto& src = value[i];
|
||||
dst.Brush = this;
|
||||
dst.Index = i;
|
||||
dst.Material = src.Material;
|
||||
dst.TexCoordScale = src.TexCoordScale;
|
||||
dst.TexCoordOffset = src.TexCoordOffset;
|
||||
dst.TexCoordRotation = src.TexCoordRotation;
|
||||
dst.ScaleInLightmap = src.ScaleInLightmap;
|
||||
}
|
||||
OnBrushModified();
|
||||
}
|
||||
|
||||
@@ -67,10 +78,7 @@ void BoxBrush::SetCenter(const Vector3& value)
|
||||
{
|
||||
if (value == _center)
|
||||
return;
|
||||
|
||||
_center = value;
|
||||
|
||||
// Fire events
|
||||
UpdateBounds();
|
||||
OnBrushModified();
|
||||
}
|
||||
@@ -79,10 +87,7 @@ void BoxBrush::SetSize(const Vector3& value)
|
||||
{
|
||||
if (value == _size)
|
||||
return;
|
||||
|
||||
_size = value;
|
||||
|
||||
// Fire events
|
||||
UpdateBounds();
|
||||
OnBrushModified();
|
||||
}
|
||||
@@ -98,7 +103,7 @@ void BoxBrush::GetSurfaces(CSG::Surface surfaces[6])
|
||||
surfaces[5].Normal = Vector3::Backward;
|
||||
|
||||
// Calculate final transformation
|
||||
const auto transform = _transform.LocalToWorld(Transform(_center, Quaternion::Identity, _size));
|
||||
const auto transform = _transform.LocalToWorld(Transform(_center, Quaternion::Identity, _size.GetAbsolute()));
|
||||
|
||||
// Set size and scale
|
||||
surfaces[0].D = surfaces[1].D = transform.Scale.X / 2;
|
||||
@@ -163,6 +168,14 @@ void BoxBrush::GetVertices(int32 surfaceIndex, Array<Vector3>& outputData) const
|
||||
}
|
||||
}
|
||||
|
||||
void BoxBrush::UpdateBounds()
|
||||
{
|
||||
OrientedBoundingBox::CreateCentered(_center, _size.GetAbsolute(), _bounds);
|
||||
_bounds.Transform(_transform);
|
||||
_bounds.GetBoundingBox(_box);
|
||||
BoundingSphere::FromBox(_box, _sphere);
|
||||
}
|
||||
|
||||
void BoxBrush::Serialize(SerializeStream& stream, const void* otherObj)
|
||||
{
|
||||
// Base
|
||||
@@ -230,7 +243,8 @@ bool BoxBrush::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)
|
||||
|
||||
void BoxBrush::OnDebugDrawSelected()
|
||||
{
|
||||
DEBUG_DRAW_WIRE_BOX(_bounds, Color::Yellow, 0, false);
|
||||
DEBUG_DRAW_WIRE_BOX(_bounds, Color::Yellow.AlphaMultiplied(0.2f), 0, false);
|
||||
DEBUG_DRAW_WIRE_BOX(_bounds, Color::Yellow, 0, true);
|
||||
|
||||
// Base
|
||||
Actor::OnDebugDrawSelected();
|
||||
|
||||
@@ -188,13 +188,7 @@ public:
|
||||
API_FUNCTION() void GetVertices(int32 surfaceIndex, API_PARAM(Out) Array<Vector3>& outputData) const;
|
||||
|
||||
private:
|
||||
FORCE_INLINE void UpdateBounds()
|
||||
{
|
||||
OrientedBoundingBox::CreateCentered(_center, _size, _bounds);
|
||||
_bounds.Transform(_transform);
|
||||
_bounds.GetBoundingBox(_box);
|
||||
BoundingSphere::FromBox(_box, _sphere);
|
||||
}
|
||||
void UpdateBounds();
|
||||
|
||||
public:
|
||||
// [Actor]
|
||||
|
||||
@@ -31,7 +31,6 @@ Font::Font(FontAsset* parentAsset, float size)
|
||||
: ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer))
|
||||
, _asset(parentAsset)
|
||||
, _size(size)
|
||||
, _characters(512)
|
||||
{
|
||||
_asset->_fonts.Add(this);
|
||||
|
||||
@@ -55,14 +54,25 @@ Font::~Font()
|
||||
void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback)
|
||||
{
|
||||
// Try to get the character or cache it if cannot be found
|
||||
if (!_characters.TryGet(c, result))
|
||||
const FontOptions& options = _asset->GetOptions();
|
||||
const auto key = Pair<float, Char>(options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : GetSize(), c);
|
||||
if (_asset->_characterCache.TryGet(key, result))
|
||||
{
|
||||
// With MSDF font introduced, cached entry may be created by a different font (with same MSDFSize)
|
||||
// This is to ensure returned entry has a reference to a font whose size matches the font being used to render
|
||||
result.Font = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This thread race condition may happen in editor but in game we usually do all stuff with fonts on main thread (chars caching)
|
||||
ScopeLock lock(_asset->Locker);
|
||||
|
||||
// Handle situation when more than one thread wants to get the same character
|
||||
if (_characters.TryGet(c, result))
|
||||
if (_asset->_characterCache.TryGet(key, result))
|
||||
{
|
||||
result.Font = this;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to use fallback font if character is missing
|
||||
if (enableFallback && !_asset->ContainsChar(c))
|
||||
@@ -70,13 +80,9 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback)
|
||||
for (int32 fallbackIndex = 0; fallbackIndex < FallbackFonts.Count(); fallbackIndex++)
|
||||
{
|
||||
FontAsset* fallbackFont = FallbackFonts.Get()[fallbackIndex].Get();
|
||||
if (fallbackFont && _asset->GetOptions().RasterMode == FontRasterMode::MSDF)
|
||||
{
|
||||
fallbackFont = fallbackFont->GetMSDF();
|
||||
}
|
||||
if (fallbackFont && fallbackFont->ContainsChar(c))
|
||||
{
|
||||
fallbackFont->CreateFont(GetSize())->GetCharacter(c, result, enableFallback);
|
||||
fallbackFont->GetRasterMode(options.RasterMode)->CreateFont(GetSize())->GetCharacter(c, result, enableFallback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -87,7 +93,7 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback)
|
||||
ASSERT(result.Font);
|
||||
|
||||
// Add to the dictionary
|
||||
_characters.Add(c, result);
|
||||
_asset->_characterCache.Add(key, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +140,32 @@ void Font::Invalidate()
|
||||
{
|
||||
ScopeLock lock(_asset->Locker);
|
||||
|
||||
for (auto i = _characters.Begin(); i.IsNotEnd(); ++i)
|
||||
// Invalidate cached characters (from atlas)
|
||||
for (auto i = _asset->_characterCache.Begin(); i.IsNotEnd(); ++i)
|
||||
{
|
||||
FontManager::Invalidate(i->Value);
|
||||
if (i->Value.Font == this)
|
||||
{
|
||||
FontManager::Invalidate(i->Value);
|
||||
_asset->_characterCache.Remove(i);
|
||||
}
|
||||
}
|
||||
_characters.Clear();
|
||||
|
||||
// Rebuild font metrics
|
||||
FlushFaceSize();
|
||||
const FT_Face face = _asset->GetFTFace();
|
||||
ASSERT(face != nullptr);
|
||||
_height = Convert26Dot6ToRoundedPixel<int32>(FT_MulFix(face->height, face->size->metrics.y_scale));
|
||||
_hasKerning = FT_HAS_KERNING(face) != 0;
|
||||
_ascender = Convert26Dot6ToRoundedPixel<int16>(face->size->metrics.ascender);
|
||||
_descender = Convert26Dot6ToRoundedPixel<int16>(face->size->metrics.descender);
|
||||
_lineGap = _height - _ascender + _descender;
|
||||
_kerningTable.Clear();
|
||||
}
|
||||
|
||||
float Font::GetScale(float layoutScale) const
|
||||
{
|
||||
const FontOptions& options = _asset->GetOptions();
|
||||
return layoutScale / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? _size / options.MSDFSize : 1.0f);
|
||||
}
|
||||
|
||||
void Font::ProcessText(const StringView& text, Array<FontLineCache, InlinedAllocation<8>>& outputLines, const TextLayoutOptions& layout)
|
||||
@@ -151,7 +178,7 @@ void Font::ProcessText(const StringView& text, Array<FontLineCache, InlinedAlloc
|
||||
FontLineCache tmpLine;
|
||||
FontCharacterEntry entry;
|
||||
FontCharacterEntry previous;
|
||||
float scale = layout.Scale / FontManager::FontScale;
|
||||
const float scale = GetScale(layout.Scale);
|
||||
float boundsWidth = layout.Bounds.GetWidth();
|
||||
float baseLinesDistance = static_cast<float>(_height) * layout.BaseLinesGapScale * scale;
|
||||
tmpLine.Location = Float2::Zero;
|
||||
@@ -195,6 +222,7 @@ void Font::ProcessText(const StringView& text, Array<FontLineCache, InlinedAlloc
|
||||
{
|
||||
// Get character entry
|
||||
GetCharacter(currentChar, entry);
|
||||
const float entryScale = entry.Font->GetScale(layout.Scale);
|
||||
|
||||
// Get kerning
|
||||
if (!isWhitespace && previous.IsValid)
|
||||
@@ -206,7 +234,7 @@ void Font::ProcessText(const StringView& text, Array<FontLineCache, InlinedAlloc
|
||||
kerning = 0;
|
||||
}
|
||||
previous = entry;
|
||||
xAdvance = (kerning + entry.AdvanceX) * scale;
|
||||
xAdvance = (kerning + entry.AdvanceX) * entryScale;
|
||||
|
||||
// Check if character fits the line or skip wrapping
|
||||
if (cursorX + xAdvance <= boundsWidth || layout.TextWrapping == TextWrapping::NoWrap)
|
||||
@@ -357,7 +385,7 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te
|
||||
Array<FontLineCache, InlinedAllocation<8>> lines;
|
||||
ProcessText(text, lines, layout);
|
||||
ASSERT(lines.HasItems());
|
||||
float scale = layout.Scale / FontManager::FontScale;
|
||||
const float scale = GetScale(layout.Scale);
|
||||
float baseLinesDistance = static_cast<float>(_height) * layout.BaseLinesGapScale * scale;
|
||||
|
||||
// Offset position to match lines origin space
|
||||
@@ -378,12 +406,13 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te
|
||||
// Cache current character
|
||||
const Char currentChar = text[currentIndex];
|
||||
GetCharacter(currentChar, entry);
|
||||
const float entryScale = entry.Font->GetScale(layout.Scale);
|
||||
const bool isWhitespace = StringUtils::IsWhitespace(currentChar);
|
||||
|
||||
// Apply kerning
|
||||
if (!isWhitespace && previous.IsValid)
|
||||
{
|
||||
x += entry.Font->GetKerning(previous.Character, entry.Character);
|
||||
x += entry.Font->GetKerning(previous.Character, entry.Character) * entryScale;
|
||||
}
|
||||
previous = entry;
|
||||
|
||||
@@ -402,7 +431,7 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te
|
||||
}
|
||||
|
||||
// Move
|
||||
x += entry.AdvanceX * scale;
|
||||
x += entry.AdvanceX * entryScale;
|
||||
}
|
||||
|
||||
// Test line end edge
|
||||
@@ -445,7 +474,7 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo
|
||||
Array<FontLineCache, InlinedAllocation<8>> lines;
|
||||
ProcessText(text, lines, layout);
|
||||
ASSERT(lines.HasItems());
|
||||
float scale = layout.Scale / FontManager::FontScale;
|
||||
const float scale = GetScale(layout.Scale);
|
||||
float baseLinesDistance = static_cast<float>(_height) * layout.BaseLinesGapScale * scale;
|
||||
|
||||
// Find line with that position
|
||||
@@ -466,17 +495,18 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo
|
||||
// Cache current character
|
||||
const Char currentChar = text[currentIndex];
|
||||
GetCharacter(currentChar, entry);
|
||||
const float entryScale = entry.Font->GetScale(layout.Scale);
|
||||
const bool isWhitespace = StringUtils::IsWhitespace(currentChar);
|
||||
|
||||
// Apply kerning
|
||||
if (!isWhitespace && previous.IsValid)
|
||||
{
|
||||
charPos.X += entry.Font->GetKerning(previous.Character, entry.Character);
|
||||
charPos.X += entry.Font->GetKerning(previous.Character, entry.Character) * entryScale;
|
||||
}
|
||||
previous = entry;
|
||||
|
||||
// Move
|
||||
charPos.X += entry.AdvanceX * scale;
|
||||
charPos.X += entry.AdvanceX * entryScale;
|
||||
}
|
||||
|
||||
// Upper left corner of the character
|
||||
@@ -492,7 +522,9 @@ void Font::FlushFaceSize() const
|
||||
{
|
||||
// Set the character size
|
||||
const FT_Face face = _asset->GetFTFace();
|
||||
const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6<FT_F26Dot6>(_size * FontManager::FontScale), DefaultDPI, DefaultDPI);
|
||||
const FontOptions& options = _asset->GetOptions();
|
||||
float size = options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : _size;
|
||||
const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6<FT_F26Dot6>(size * FontManager::FontScale), DefaultDPI, DefaultDPI);
|
||||
if (error)
|
||||
{
|
||||
LOG_FT_ERROR(error);
|
||||
|
||||
+10
-108
@@ -10,7 +10,7 @@
|
||||
#include "TextLayoutOptions.h"
|
||||
|
||||
class FontAsset;
|
||||
struct FontTextureAtlasSlot;
|
||||
struct FontCharacterEntry;
|
||||
|
||||
// The default DPI that engine is using
|
||||
#define DefaultDPI 96
|
||||
@@ -119,110 +119,6 @@ struct TIsPODType<FontLineCache>
|
||||
enum { Value = true };
|
||||
};
|
||||
|
||||
// Font glyph metrics:
|
||||
//
|
||||
// xmin xmax
|
||||
// | |
|
||||
// |<-------- width -------->|
|
||||
// | |
|
||||
// | +-------------------------+----------------- ymax
|
||||
// | | ggggggggg ggggg | ^ ^
|
||||
// | | g:::::::::ggg::::g | | |
|
||||
// | | g:::::::::::::::::g | | |
|
||||
// | | g::::::ggggg::::::gg | | |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// offsetX -|-------->| g:::::g g:::::g | offsetY |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// | | g::::::g g:::::g | | |
|
||||
// | | g:::::::ggggg:::::g | | |
|
||||
// | | g::::::::::::::::g | | height
|
||||
// | | gg::::::::::::::g | | |
|
||||
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
|
||||
// / | | g:::::g | |
|
||||
// origin | | gggggg g:::::g | |
|
||||
// | | g:::::gg gg:::::g | |
|
||||
// | | g::::::ggg:::::::g | |
|
||||
// | | gg:::::::::::::g | |
|
||||
// | | ggg::::::ggg | |
|
||||
// | | gggggg | v
|
||||
// | +-------------------------+----------------- ymin
|
||||
// | |
|
||||
// |------------- advanceX ----------->|
|
||||
|
||||
/// <summary>
|
||||
/// The cached font character entry (read for rendering and further processing).
|
||||
/// </summary>
|
||||
API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry);
|
||||
|
||||
/// <summary>
|
||||
/// The character represented by this entry.
|
||||
/// </summary>
|
||||
API_FIELD() Char Character;
|
||||
|
||||
/// <summary>
|
||||
/// True if entry is valid, otherwise false.
|
||||
/// </summary>
|
||||
API_FIELD() bool IsValid = false;
|
||||
|
||||
/// <summary>
|
||||
/// The index to a specific texture in the font cache.
|
||||
/// </summary>
|
||||
API_FIELD() byte TextureIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The left bearing expressed in integer pixels.
|
||||
/// </summary>
|
||||
API_FIELD() int16 OffsetX;
|
||||
|
||||
/// <summary>
|
||||
/// The top bearing expressed in integer pixels.
|
||||
/// </summary>
|
||||
API_FIELD() int16 OffsetY;
|
||||
|
||||
/// <summary>
|
||||
/// The amount to advance in X before drawing the next character in a string.
|
||||
/// </summary>
|
||||
API_FIELD() int16 AdvanceX;
|
||||
|
||||
/// <summary>
|
||||
/// The distance from baseline to glyph top most point.
|
||||
/// </summary>
|
||||
API_FIELD() int16 BearingY;
|
||||
|
||||
/// <summary>
|
||||
/// The height in pixels of the glyph.
|
||||
/// </summary>
|
||||
API_FIELD() int16 Height;
|
||||
|
||||
/// <summary>
|
||||
/// The start location of the character in the texture (in texture coordinates space).
|
||||
/// </summary>
|
||||
API_FIELD() Float2 UV;
|
||||
|
||||
/// <summary>
|
||||
/// The size the character in the texture (in texture coordinates space).
|
||||
/// </summary>
|
||||
API_FIELD() Float2 UVSize;
|
||||
|
||||
/// <summary>
|
||||
/// The slot in texture atlas, containing the pixel data of the glyph.
|
||||
/// </summary>
|
||||
API_FIELD() const FontTextureAtlasSlot* Slot;
|
||||
|
||||
/// <summary>
|
||||
/// The owner font.
|
||||
/// </summary>
|
||||
API_FIELD() const class Font* Font;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TIsPODType<FontCharacterEntry>
|
||||
{
|
||||
enum { Value = true };
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Represents font object that can be using during text rendering (it uses Font Asset but with pre-cached data for chosen font properties).
|
||||
/// </summary>
|
||||
@@ -239,7 +135,6 @@ private:
|
||||
int32 _descender;
|
||||
int32 _lineGap;
|
||||
bool _hasKerning;
|
||||
Dictionary<Char, FontCharacterEntry> _characters;
|
||||
mutable Dictionary<uint32, int32> _kerningTable;
|
||||
|
||||
public:
|
||||
@@ -327,7 +222,7 @@ public:
|
||||
/// </summary>
|
||||
/// <param name="c">The character.</param>
|
||||
/// <param name="result">The output character entry.</param>
|
||||
/// <param name="enableFallback">True if fallback to secondary font when the primary font doesn't contains this character.</param>
|
||||
/// <param name="enableFallback">True if fallback to secondary font when the primary font doesn't contain this character.</param>
|
||||
void GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback = true);
|
||||
|
||||
/// <summary>
|
||||
@@ -345,11 +240,18 @@ public:
|
||||
API_FUNCTION() void CacheText(const StringView& text);
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates all cached dynamic font atlases using this font. Can be used to reload font characters after changing font asset options.
|
||||
/// Refresh cached metrics and invalidates all cached characters (in atlases). Can be used after changing font asset options.
|
||||
/// </summary>
|
||||
API_FUNCTION() void Invalidate();
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Gets the scale factor that maps the rasterized font size to the actual rendered size.
|
||||
/// </summary>
|
||||
/// <param name="layoutScale">The layout scale.</param>
|
||||
/// <returns>The scale factor.</returns>
|
||||
float GetScale(float layoutScale) const;
|
||||
|
||||
/// <summary>
|
||||
/// Processes text to get cached lines for rendering.
|
||||
/// </summary>
|
||||
|
||||
@@ -69,7 +69,7 @@ void FontAsset::unload(bool isReloading)
|
||||
_fontFile.Release();
|
||||
_virtualBold = nullptr;
|
||||
_virtualItalic = nullptr;
|
||||
_virtualMSDF = nullptr;
|
||||
_virtualRasterMode = nullptr;
|
||||
}
|
||||
|
||||
AssetChunksFlag FontAsset::getChunksToPreload() const
|
||||
@@ -101,6 +101,26 @@ FontFlags FontAsset::GetStyle() const
|
||||
void FontAsset::SetOptions(const FontOptions& value)
|
||||
{
|
||||
_options = value;
|
||||
Invalidate();
|
||||
|
||||
if (_virtualBold)
|
||||
{
|
||||
auto options = _options;
|
||||
options.Flags |= FontFlags::Bold;
|
||||
_virtualBold->SetOptions(options);
|
||||
}
|
||||
if (_virtualItalic)
|
||||
{
|
||||
auto options = _options;
|
||||
options.Flags |= FontFlags::Italic;
|
||||
_virtualItalic->SetOptions(options);
|
||||
}
|
||||
if (_virtualRasterMode)
|
||||
{
|
||||
auto options = _options;
|
||||
options.RasterMode = _options.RasterMode == FontRasterMode::MSDF ? FontRasterMode::Bitmap : FontRasterMode::MSDF;
|
||||
_virtualRasterMode->SetOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
Font* FontAsset::CreateFont(float size)
|
||||
@@ -156,20 +176,20 @@ FontAsset* FontAsset::GetItalic()
|
||||
return _virtualItalic;
|
||||
}
|
||||
|
||||
FontAsset* FontAsset::GetMSDF()
|
||||
FontAsset* FontAsset::GetRasterMode(FontRasterMode rasterMode)
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
if (_options.RasterMode == FontRasterMode::MSDF)
|
||||
if (_options.RasterMode == rasterMode)
|
||||
return this;
|
||||
if (!_virtualMSDF)
|
||||
if (!_virtualRasterMode)
|
||||
{
|
||||
_virtualMSDF = Content::CreateVirtualAsset<FontAsset>();
|
||||
_virtualMSDF->Init(_fontFile);
|
||||
_virtualRasterMode = Content::CreateVirtualAsset<FontAsset>();
|
||||
_virtualRasterMode->Init(_fontFile);
|
||||
auto options = _options;
|
||||
options.RasterMode = FontRasterMode::MSDF;
|
||||
_virtualMSDF->SetOptions(options);
|
||||
options.RasterMode = rasterMode;
|
||||
_virtualRasterMode->SetOptions(options);
|
||||
}
|
||||
return _virtualMSDF;
|
||||
return _virtualRasterMode;
|
||||
}
|
||||
|
||||
bool FontAsset::Init(const BytesContainer& fontFile)
|
||||
@@ -214,6 +234,13 @@ bool FontAsset::ContainsChar(Char c) const
|
||||
void FontAsset::Invalidate()
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
// Invalidate cached characters (from atlas)
|
||||
for (auto& entry : _characterCache)
|
||||
FontManager::Invalidate(entry.Value);
|
||||
_characterCache.Clear();
|
||||
|
||||
// Refresh cached metrics of all fonts created from this asset
|
||||
for (auto font : _fonts)
|
||||
font->Invalidate();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace FlaxEngine
|
||||
/// <returns><c>true</c> if this object has the same value as <paramref name="other" />; otherwise, <c>false</c> </returns>
|
||||
public bool Equals(FontOptions other)
|
||||
{
|
||||
return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode;
|
||||
return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode && MSDFSize == other.MSDFSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -25,7 +25,7 @@ namespace FlaxEngine
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode);
|
||||
return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode, MSDFSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,7 +36,7 @@ namespace FlaxEngine
|
||||
/// <returns><c>true</c> if <paramref name="left" /> has the same value as <paramref name="right" />; otherwise, <c>false</c>.</returns>
|
||||
public static bool operator ==(FontOptions left, FontOptions right)
|
||||
{
|
||||
return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode;
|
||||
return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode && left.MSDFSize == right.MSDFSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +47,7 @@ namespace FlaxEngine
|
||||
/// <returns><c>true</c> if <paramref name="left" /> has a different value than <paramref name="right" />; otherwise,<c>false</c>.</returns>
|
||||
public static bool operator !=(FontOptions left, FontOptions right)
|
||||
{
|
||||
return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode;
|
||||
return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode || left.MSDFSize != right.MSDFSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
#include "Engine/Content/BinaryAsset.h"
|
||||
#include "Engine/Content/AssetReference.h"
|
||||
#include "Engine/Core/Collections/Array.h"
|
||||
#include "Engine/Core/Collections/Dictionary.h"
|
||||
#include "Engine/Core/Math/Vector2.h"
|
||||
#include "Engine/Render2D/FontCharacterEntry.h"
|
||||
|
||||
class Font;
|
||||
class FontManager;
|
||||
@@ -105,6 +109,11 @@ API_STRUCT() struct FontOptions
|
||||
/// The font rasterization mode.
|
||||
/// </summary>
|
||||
API_FIELD() FontRasterMode RasterMode;
|
||||
|
||||
/// <summary>
|
||||
/// The font size used when generating MSDF font atlases.
|
||||
/// </summary>
|
||||
API_FIELD() float MSDFSize;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -112,7 +121,7 @@ API_STRUCT() struct FontOptions
|
||||
/// </summary>
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(FontAsset, 4);
|
||||
DECLARE_BINARY_ASSET_HEADER(FontAsset, 5);
|
||||
friend Font;
|
||||
|
||||
private:
|
||||
@@ -120,9 +129,10 @@ private:
|
||||
FontOptions _options;
|
||||
BytesContainer _fontFile;
|
||||
Array<Font*, InlinedAllocation<32>> _fonts;
|
||||
Dictionary<Pair<float, Char>, FontCharacterEntry> _characterCache;
|
||||
AssetReference<FontAsset> _virtualBold;
|
||||
AssetReference<FontAsset> _virtualItalic;
|
||||
AssetReference<FontAsset> _virtualMSDF;
|
||||
AssetReference<FontAsset> _virtualRasterMode;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -182,10 +192,10 @@ public:
|
||||
API_FUNCTION() FontAsset* GetItalic();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MSDF version of the font. Returns itself or creates a new virtual font asset using this font but rasterized with Multi-channel Signed Distance Field (MSDF).
|
||||
/// Gets the different rasterization mode of the font. Returns itself or creates a new virtual font asset using this font but rasterized with the specified mode.
|
||||
/// </summary>
|
||||
/// <returns>The virtual font or this.</returns>
|
||||
API_FUNCTION() FontAsset* GetMSDF();
|
||||
API_FUNCTION() FontAsset* GetRasterMode(FontRasterMode rasterMode);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the font with a custom font file data.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Engine/Core/Math/Vector2.h"
|
||||
|
||||
// Font glyph metrics:
|
||||
//
|
||||
// xmin xmax
|
||||
// | |
|
||||
// |<-------- width -------->|
|
||||
// | |
|
||||
// | +-------------------------+----------------- ymax
|
||||
// | | ggggggggg ggggg | ^ ^
|
||||
// | | g:::::::::ggg::::g | | |
|
||||
// | | g:::::::::::::::::g | | |
|
||||
// | | g::::::ggggg::::::gg | | |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// offsetX -|-------->| g:::::g g:::::g | offsetY |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// | | g::::::g g:::::g | | |
|
||||
// | | g:::::::ggggg:::::g | | |
|
||||
// | | g::::::::::::::::g | | height
|
||||
// | | gg::::::::::::::g | | |
|
||||
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
|
||||
// / | | g:::::g | |
|
||||
// origin | | gggggg g:::::g | |
|
||||
// | | g:::::gg gg:::::g | |
|
||||
// | | g::::::ggg:::::::g | |
|
||||
// | | gg:::::::::::::g | |
|
||||
// | | ggg::::::ggg | |
|
||||
// | | gggggg | v
|
||||
// | +-------------------------+----------------- ymin
|
||||
// | |
|
||||
// |------------- advanceX ----------->|
|
||||
|
||||
/// <summary>
|
||||
/// The cached font character entry (read for rendering and further processing).
|
||||
/// </summary>
|
||||
API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry
|
||||
{
|
||||
DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry);
|
||||
|
||||
/// <summary>
|
||||
/// The character represented by this entry.
|
||||
/// </summary>
|
||||
API_FIELD() Char Character;
|
||||
|
||||
/// <summary>
|
||||
/// True if entry is valid, otherwise false.
|
||||
/// </summary>
|
||||
API_FIELD() bool IsValid = false;
|
||||
|
||||
/// <summary>
|
||||
/// The index to a specific texture in the font cache.
|
||||
/// </summary>
|
||||
API_FIELD() byte TextureIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The left bearing expressed in integer pixels.
|
||||
/// </summary>
|
||||
API_FIELD() int16 OffsetX;
|
||||
|
||||
/// <summary>
|
||||
/// The top bearing expressed in integer pixels.
|
||||
/// </summary>
|
||||
API_FIELD() int16 OffsetY;
|
||||
|
||||
/// <summary>
|
||||
/// The amount to advance in X before drawing the next character in a string.
|
||||
/// </summary>
|
||||
API_FIELD() int16 AdvanceX;
|
||||
|
||||
/// <summary>
|
||||
/// The distance from baseline to glyph top most point.
|
||||
/// </summary>
|
||||
API_FIELD() int16 BearingY;
|
||||
|
||||
/// <summary>
|
||||
/// The height in pixels of the glyph.
|
||||
/// </summary>
|
||||
API_FIELD() int16 Height;
|
||||
|
||||
/// <summary>
|
||||
/// The start location of the character in the texture (in texture coordinates space).
|
||||
/// </summary>
|
||||
API_FIELD() Float2 UV;
|
||||
|
||||
/// <summary>
|
||||
/// The size the character in the texture (in texture coordinates space).
|
||||
/// </summary>
|
||||
API_FIELD() Float2 UVSize;
|
||||
|
||||
/// <summary>
|
||||
/// The slot in texture atlas, containing the pixel data of the glyph.
|
||||
/// </summary>
|
||||
API_FIELD() const struct FontTextureAtlasSlot* Slot;
|
||||
|
||||
/// <summary>
|
||||
/// The owner font.
|
||||
/// </summary>
|
||||
API_FIELD() const class Font* Font;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TIsPODType<FontCharacterEntry>
|
||||
{
|
||||
enum { Value = true };
|
||||
};
|
||||
@@ -1203,7 +1203,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
Float2 invAtlasSize = Float2::One;
|
||||
FontCharacterEntry previous;
|
||||
int32 kerning;
|
||||
float scale = 1.0f / FontManager::FontScale;
|
||||
const FontOptions& options = font->GetAsset()->GetOptions();
|
||||
const float scale = font->GetScale(1.0f);
|
||||
const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts);
|
||||
|
||||
// Render all characters
|
||||
@@ -1216,7 +1217,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
}
|
||||
else
|
||||
{
|
||||
drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar;
|
||||
drawCall.Type = options.RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar;
|
||||
drawCall.AsChar.Mat = nullptr;
|
||||
}
|
||||
Float2 pointer = location;
|
||||
@@ -1231,6 +1232,9 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
// Get character entry
|
||||
font->GetCharacter(currentChar, entry, enableFallbackFonts);
|
||||
|
||||
// Fallback fonts may have different MSDFSize, so we need to calculate scale per character
|
||||
const float entryScale = entry.Font->GetScale(1.0f);
|
||||
|
||||
// Check if need to select/change font atlas (since characters even in the same font may be located in different atlases)
|
||||
if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex)
|
||||
{
|
||||
@@ -1262,18 +1266,16 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
{
|
||||
kerning = 0;
|
||||
}
|
||||
pointer.X += kerning * scale;
|
||||
pointer.X += kerning * entryScale;
|
||||
previous = entry;
|
||||
|
||||
// Omit whitespace characters
|
||||
if (!isWhitespace)
|
||||
{
|
||||
// Calculate character size and atlas coordinates
|
||||
const float x = pointer.X + entry.OffsetX * scale;
|
||||
const float y = pointer.Y + (font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale;
|
||||
|
||||
Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale);
|
||||
|
||||
const float x = pointer.X + entry.OffsetX * entryScale;
|
||||
const float y = pointer.Y - entry.OffsetY * entryScale + (font->GetHeight() + font->GetDescender()) * scale;
|
||||
Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale);
|
||||
Float2 upperLeftUV = entry.UV * invAtlasSize;
|
||||
Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize;
|
||||
|
||||
@@ -1285,7 +1287,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
}
|
||||
|
||||
// Move
|
||||
pointer.X += entry.AdvanceX * scale;
|
||||
pointer.X += entry.AdvanceX * entryScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1318,7 +1320,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
Float2 invAtlasSize = Float2::One;
|
||||
FontCharacterEntry previous;
|
||||
int32 kerning;
|
||||
float scale = layout.Scale / FontManager::FontScale;
|
||||
const FontOptions& options = font->GetAsset()->GetOptions();
|
||||
const float scale = font->GetScale(layout.Scale);
|
||||
const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts);
|
||||
|
||||
// Process text to get lines
|
||||
@@ -1335,7 +1338,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
}
|
||||
else
|
||||
{
|
||||
drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar;
|
||||
drawCall.Type = options.RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar;
|
||||
drawCall.AsChar.Mat = nullptr;
|
||||
}
|
||||
for (int32 lineIndex = 0; lineIndex < Lines.Count(); lineIndex++)
|
||||
@@ -1355,6 +1358,9 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
// Get character entry
|
||||
font->GetCharacter(currentChar, entry, enableFallbackFonts);
|
||||
|
||||
// Fallback fonts may have different MSDFSize, so we need to calculate scale per character
|
||||
const float entryScale = entry.Font->GetScale(layout.Scale);
|
||||
|
||||
// Check if need to select/change font atlas (since characters even in the same font may be located in different atlases)
|
||||
if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex)
|
||||
{
|
||||
@@ -1384,19 +1390,17 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
{
|
||||
kerning = 0;
|
||||
}
|
||||
pointer.X += (float)kerning * scale;
|
||||
pointer.X += (float)kerning * entryScale;
|
||||
previous = entry;
|
||||
|
||||
// Omit whitespace characters
|
||||
if (!isWhitespace)
|
||||
{
|
||||
// Calculate character size and atlas coordinates
|
||||
const float x = pointer.X + entry.OffsetX * scale;
|
||||
const float y = pointer.Y - entry.OffsetY * scale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale);
|
||||
|
||||
Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale);
|
||||
const float x = pointer.X + entry.OffsetX * entryScale;
|
||||
const float y = pointer.Y - entry.OffsetY * entryScale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale);
|
||||
Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale);
|
||||
charRect.Offset(layout.Bounds.Location);
|
||||
|
||||
Float2 upperLeftUV = entry.UV * invAtlasSize;
|
||||
Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize;
|
||||
|
||||
@@ -1408,7 +1412,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
|
||||
}
|
||||
|
||||
// Move
|
||||
pointer.X += entry.AdvanceX * scale;
|
||||
pointer.X += entry.AdvanceX * entryScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +575,7 @@ bool TextureTool::ImportTextureStb(ImageType type, Span<byte> bytes, TextureData
|
||||
}
|
||||
}
|
||||
|
||||
// Use two data containers for texture importing for more optimzied performance
|
||||
// Use two data containers for texture importing for more optimized performance
|
||||
TextureData textureDataTmp;
|
||||
TextureData* textureDataSrc = &textureData;
|
||||
TextureData* textureDataDst = &textureDataTmp;
|
||||
|
||||
@@ -595,7 +595,7 @@ namespace FlaxEngine.GUI
|
||||
Size = new Float2(size.X - margin, size.Y),
|
||||
Font = Font,
|
||||
TextColor = TextColor * 0.9f,
|
||||
TextColorHighlighted = BackgroundColorSelected.Brightness < 0.05f ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted,
|
||||
TextColorHighlighted = (Mathf.IsZero(BackgroundColorSelected.Brightness) || BackgroundColorSelected.IsTransparent) ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted,
|
||||
HorizontalAlignment = HorizontalAlignment,
|
||||
VerticalAlignment = VerticalAlignment,
|
||||
Text = _items[i],
|
||||
|
||||
@@ -225,37 +225,123 @@ namespace FlaxEngine.GUI
|
||||
var lines = font.ProcessText(_text, ref textBlock.Range);
|
||||
if (lines == null || lines.Length == 0)
|
||||
return;
|
||||
var wrapWidth = _wrapping != TextWrapping.NoWrap ? Width : -1.0f;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
ref var line = ref lines[i];
|
||||
textBlock.Range = new TextRange
|
||||
var lineRange = new TextRange
|
||||
{
|
||||
StartIndex = start + line.FirstCharIndex,
|
||||
EndIndex = start + line.LastCharIndex + 1,
|
||||
};
|
||||
|
||||
if (i != 0)
|
||||
{
|
||||
context.Caret.X = 0;
|
||||
OnLineAdded(ref context, textBlock.Range.StartIndex - 1);
|
||||
OnLineAdded(ref context, lineRange.StartIndex - 1);
|
||||
}
|
||||
textBlock.Bounds = new Rectangle(context.Caret, line.Size);
|
||||
textBlock.Bounds.X += line.Location.X;
|
||||
|
||||
context.AddTextBlock(ref textBlock);
|
||||
}
|
||||
|
||||
// Update the caret location
|
||||
ref var lastLine = ref lines[lines.Length - 1];
|
||||
if (lines.Length == 1)
|
||||
{
|
||||
context.Caret.X += lastLine.Size.X;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Caret.X = lastLine.Size.X;
|
||||
if (wrapWidth > 0 && context.Caret.X + line.Size.X > wrapWidth)
|
||||
{
|
||||
// Line overflows the available width - split it into multiple wrapped lines
|
||||
AddWrappedTextBlocks(ref context, ref textBlock, font, lineRange, wrapWidth, _wrapping);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add block
|
||||
textBlock.Range = lineRange;
|
||||
textBlock.Bounds = new Rectangle(context.Caret, line.Size);
|
||||
textBlock.Bounds.X += line.Location.X;
|
||||
context.AddTextBlock(ref textBlock);
|
||||
context.Caret.X += line.Size.X;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the given text range into multiple text blocks (lines) so it fits within the available wrapping width, correctly continuing from the current caret position (eg. when a differently-styled text run follows on the same line).
|
||||
/// </summary>
|
||||
/// <param name="context">The parsing context.</param>
|
||||
/// <param name="textBlock">The template text block (style is reused, range and bounds get overriden per produced block).</param>
|
||||
/// <param name="font">The font used to measure and render the text.</param>
|
||||
/// <param name="range">The text range to wrap (single logical line - no explicit newlines inside).</param>
|
||||
/// <param name="wrapWidth">The maximum available width (in control-space) that a single visual line can use.</param>
|
||||
/// <param name="wrapping">The wrapping mode - either whole-word (breaks only at whitespace) or per-character (can break in the middle of a word).</param>
|
||||
private void AddWrappedTextBlocks(ref ParsingContext context, ref TextBlock textBlock, Font font, TextRange range, float wrapWidth, TextWrapping wrapping)
|
||||
{
|
||||
int segmentStart = range.StartIndex;
|
||||
float segmentWidth = 0.0f;
|
||||
int pos = range.StartIndex;
|
||||
while (pos < range.EndIndex)
|
||||
{
|
||||
// Consume the next "chunk" - a single character for WrapChars, or a whole word (plus trailing whitespace) for WrapWords
|
||||
int chunkStart = pos;
|
||||
if (wrapping == TextWrapping.WrapChars)
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
else
|
||||
{
|
||||
while (pos < range.EndIndex && !char.IsWhiteSpace(_text[pos]))
|
||||
pos++;
|
||||
while (pos < range.EndIndex && char.IsWhiteSpace(_text[pos]))
|
||||
pos++;
|
||||
}
|
||||
|
||||
var chunkRange = new TextRange { StartIndex = chunkStart, EndIndex = pos };
|
||||
var chunkWidth = font.MeasureText(_text, ref chunkRange).X;
|
||||
|
||||
if ((segmentWidth > 0.0f || context.Caret.X > 0.0f) && context.Caret.X + segmentWidth + chunkWidth > wrapWidth)
|
||||
{
|
||||
// The next chunk no longer fits - emit the accumulated segment (if any) and start a new line
|
||||
if (segmentStart < chunkStart)
|
||||
AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, chunkStart);
|
||||
context.Caret.X = 0;
|
||||
OnLineAdded(ref context, chunkStart - 1);
|
||||
segmentStart = chunkStart;
|
||||
segmentWidth = 0.0f;
|
||||
}
|
||||
segmentWidth += chunkWidth;
|
||||
|
||||
// For Wrap Words mode: A single word wider than the whole available width can't be split further, so force it onto its own line
|
||||
if (wrapping == TextWrapping.WrapWords && context.Caret.X <= 0.0f && segmentStart == chunkStart && segmentWidth > wrapWidth && pos < range.EndIndex)
|
||||
{
|
||||
AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, pos);
|
||||
context.Caret.X = 0;
|
||||
OnLineAdded(ref context, pos - 1);
|
||||
segmentStart = pos;
|
||||
segmentWidth = 0.0f;
|
||||
}
|
||||
}
|
||||
if (segmentStart < range.EndIndex)
|
||||
AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, range.EndIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single text block to the control, using the current caret position as the origin and moving the caret forward by the width of the text block.
|
||||
/// </summary>
|
||||
/// <param name="context">The parsing context.</param>
|
||||
/// <param name="textBlock">The text block to add.</param>
|
||||
/// <param name="font">The font to use for measurement.</param>
|
||||
/// <param name="start">The start index of the text range.</param>
|
||||
/// <param name="end">The end index of the text range.</param>
|
||||
private void AddWrappedTextBlock(ref ParsingContext context, ref TextBlock textBlock, Font font, int start, int end)
|
||||
{
|
||||
var range = new TextRange { StartIndex = start, EndIndex = end };
|
||||
var size = font.MeasureText(_text, ref range);
|
||||
textBlock.Range = range;
|
||||
textBlock.Bounds = new Rectangle(context.Caret, size);
|
||||
context.AddTextBlock(ref textBlock);
|
||||
context.Caret.X += size.X;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a new line is added (eg. after a newline character or when the text overflows the available width).
|
||||
/// It organizes the text blocks within the line and moves the caret to the next line.
|
||||
/// </summary>
|
||||
/// <param name="context">The parsing context.</param>
|
||||
/// <param name="lineEnd">The index of the last character in the line.</param>
|
||||
private void OnLineAdded(ref ParsingContext context, int lineEnd)
|
||||
{
|
||||
// Calculate size of the line
|
||||
@@ -333,7 +419,7 @@ namespace FlaxEngine.GUI
|
||||
// Move to the next line
|
||||
context.LineStartCharacterIndex = lineEnd + 1;
|
||||
context.LineStartTextBlockIndex = _textBlocks.Count;
|
||||
context.Caret.Y += lineSize.Y;
|
||||
context.Caret.Y += lineSize.Y * _baseLinesGapScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace FlaxEngine.GUI
|
||||
{
|
||||
@@ -11,6 +12,8 @@ namespace FlaxEngine.GUI
|
||||
public partial class RichTextBox : RichTextBoxBase
|
||||
{
|
||||
private TextBlockStyle _textStyle;
|
||||
private TextWrapping _wrapping = TextWrapping.NoWrap;
|
||||
private float _baseLinesGapScale = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The default text style applied to the whole text.
|
||||
@@ -26,16 +29,50 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text wrapping within the control bounds.
|
||||
/// </summary>
|
||||
[DefaultValue(TextWrapping.NoWrap)]
|
||||
[EditorOrder(30), Tooltip("The text wrapping within the control bounds.")]
|
||||
public TextWrapping Wrapping
|
||||
{
|
||||
get => _wrapping;
|
||||
set
|
||||
{
|
||||
if (_wrapping == value)
|
||||
return;
|
||||
_wrapping = value;
|
||||
UpdateTextBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gap between lines when wrapping and more than a single line is displayed.
|
||||
/// </summary>
|
||||
[DefaultValue(1.0f)]
|
||||
[EditorOrder(40), Tooltip("The gap between lines when wrapping and more than a single line is displayed."), Limit(0f, 100.0f, 0.01f)]
|
||||
public float BaseLinesGapScale
|
||||
{
|
||||
get => _baseLinesGapScale;
|
||||
set
|
||||
{
|
||||
if (Mathf.NearEqual(_baseLinesGapScale, value))
|
||||
return;
|
||||
_baseLinesGapScale = value;
|
||||
UpdateTextBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The collection of custom text styles to apply (named).
|
||||
/// </summary>
|
||||
[EditorOrder(30)]
|
||||
[EditorOrder(50)]
|
||||
public Dictionary<string, TextBlockStyle> Styles = new Dictionary<string, TextBlockStyle>();
|
||||
|
||||
/// <summary>
|
||||
/// The collection of custom images/sprites that can be inlined in text (named).
|
||||
/// </summary>
|
||||
[EditorOrder(40)]
|
||||
[EditorOrder(60)]
|
||||
public Dictionary<string, IBrush> Images = new Dictionary<string, IBrush>();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,6 +29,11 @@ namespace FlaxEngine.GUI
|
||||
[HideInEditor]
|
||||
public ParseTextBlocksDelegate ParseTextBlocks;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of parsed text blocks.
|
||||
/// </summary>
|
||||
public List<TextBlock> TextBlocks => _textBlocks;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RichTextBoxBase"/> class.
|
||||
/// </summary>
|
||||
@@ -95,15 +100,19 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
|
||||
// Handle case when index is outside all text ranges
|
||||
if (index >= 0 && blockCount > 0 && index <= textBlocksSpan[0].Range.StartIndex)
|
||||
if (index >= 0 && blockCount > 0)
|
||||
{
|
||||
result = textBlocksSpan[0];
|
||||
return true;
|
||||
}
|
||||
if (index >= 0 && blockCount > 0 && index >= textBlocksSpan[blockCount - 1].Range.StartIndex)
|
||||
{
|
||||
result = textBlocksSpan[blockCount - 1];
|
||||
return true;
|
||||
if (index <= textBlocksSpan[0].Range.StartIndex)
|
||||
{
|
||||
result = textBlocksSpan[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index >= textBlocksSpan[blockCount - 1].Range.StartIndex)
|
||||
{
|
||||
result = textBlocksSpan[blockCount - 1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no text block is found
|
||||
|
||||
@@ -463,6 +463,8 @@ namespace FlaxEngine.GUI
|
||||
// Click change
|
||||
Value = _value + (mousePosition < _thumbCenter ? -1 : 1) * _clickChange;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(location, button);
|
||||
|
||||
@@ -175,7 +175,7 @@ void TextRender::UpdateLayout()
|
||||
|
||||
// Pick a font (remove DPI text scale as the text is being placed in the world)
|
||||
auto font = Font->CreateFont(_size);
|
||||
float scale = _layoutOptions.Scale / FontManager::FontScale;
|
||||
const float scale = font->GetScale(_layoutOptions.Scale);
|
||||
|
||||
// Prepare
|
||||
FontTextureAtlas* fontAtlas = nullptr;
|
||||
@@ -220,6 +220,7 @@ void TextRender::UpdateLayout()
|
||||
if (c != '\n')
|
||||
{
|
||||
font->GetCharacter(c, entry);
|
||||
const float entryScale = entry.Font->GetScale(_layoutOptions.Scale);
|
||||
|
||||
// Check if need to select/change font atlas (since characters even in the same font may be located in different atlases)
|
||||
if (fontAtlas == nullptr || entry.TextureIndex != drawChunk.FontAtlasIndex)
|
||||
@@ -276,17 +277,17 @@ void TextRender::UpdateLayout()
|
||||
{
|
||||
kerning = 0;
|
||||
}
|
||||
pointer.X += (float)kerning * scale;
|
||||
pointer.X += (float)kerning * entryScale;
|
||||
previous = entry;
|
||||
|
||||
// Omit whitespace characters
|
||||
if (!isWhitespace)
|
||||
{
|
||||
// Calculate character size and atlas coordinates
|
||||
const float x = pointer.X + (float)entry.OffsetX * scale;
|
||||
const float y = pointer.Y + (float)(font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale;
|
||||
const float x = pointer.X + (float)entry.OffsetX * entryScale;
|
||||
const float y = pointer.Y - (float)entry.OffsetY * entryScale + (float)(font->GetHeight() + font->GetDescender()) * scale;
|
||||
|
||||
Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale);
|
||||
Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale);
|
||||
charRect.Offset(_layoutOptions.Bounds.Location);
|
||||
|
||||
Float2 upperLeftUV = entry.UV * invAtlasSize;
|
||||
@@ -329,7 +330,7 @@ void TextRender::UpdateLayout()
|
||||
}
|
||||
|
||||
// Move
|
||||
pointer.X += (float)entry.AdvanceX * scale;
|
||||
pointer.X += (float)entry.AdvanceX * entryScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -141,9 +141,9 @@ int mode, detexBlock128 * DETEX_RESTRICT block) {
|
||||
// 64-bit word boundary in any mode.
|
||||
uint32_t bits;
|
||||
if (block->index < 64)
|
||||
bits = block->data0 >> block->index;
|
||||
bits = (uint32_t)(block->data0 >> block->index);
|
||||
else
|
||||
bits = block->data1 >> (block->index - 64);
|
||||
bits = (uint32_t)(block->data1 >> (block->index - 64));
|
||||
for (int i = 0; i < nu_subsets * 2; i++) {
|
||||
endpoint_array[i * 4 + 0] <<= 1;
|
||||
endpoint_array[i * 4 + 1] <<= 1;
|
||||
|
||||
@@ -671,7 +671,11 @@ namespace Flax.Build.Bindings
|
||||
else if (nativeType == "object[]")
|
||||
parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemObjectArrayMarshaller))";
|
||||
else if (parameterInfo.Type.Type == "Array" && parameterInfo.Type.GenericArgs.Count > 0 && parameterInfo.Type.GenericArgs[0].Type == "bool")
|
||||
{
|
||||
parameterMarshalType = $"MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = {(!functionInfo.IsStatic ? 1 : 0) + functionInfo.Parameters.Count + (functionInfo.Glue.CustomParameters.FindIndex(x => x.Name == $"__{parameterInfo.Name}Count"))})";
|
||||
if (!parameterInfo.IsOut && !parameterInfo.IsRef)
|
||||
parameterMarshalType += ", In"; // The usage of 'LibraryImportAttribute' does not follow recommendations. It is recommended to use explicit '[In]' and '[Out]' attributes on array parameters.
|
||||
}
|
||||
else if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer" || nativeType == "Array")
|
||||
{
|
||||
parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = \"__{parameterInfo.Name}Count\")";
|
||||
|
||||
Reference in New Issue
Block a user