diff --git a/Content/Editor/IconsAtlas.flax b/Content/Editor/IconsAtlas.flax
index ae46fcf06..bbac95de6 100644
--- a/Content/Editor/IconsAtlas.flax
+++ b/Content/Editor/IconsAtlas.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:745ce418c493445abde98aea91a34592b00c5c46b68cd7e61604a05b3043c76c
-size 5608587
+oid sha256:0f43bf2050d47a1e4d6db35e5da66c2f999236939b9d962cd0fba56dcb171852
+size 5611913
diff --git a/Source/Editor/Content/GUI/ContentNavigationButton.cs b/Source/Editor/Content/GUI/ContentNavigation.cs
similarity index 58%
rename from Source/Editor/Content/GUI/ContentNavigationButton.cs
rename to Source/Editor/Content/GUI/ContentNavigation.cs
index d37e1629b..7adf6aa62 100644
--- a/Source/Editor/Content/GUI/ContentNavigationButton.cs
+++ b/Source/Editor/Content/GUI/ContentNavigation.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI;
+using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -31,7 +32,7 @@ namespace FlaxEditor.Content.GUI
: base(x, y, height)
{
TargetNode = targetNode;
- Text = targetNode.NavButtonLabel + "/";
+ Text = targetNode.NavButtonLabel;
}
///
@@ -72,7 +73,6 @@ namespace FlaxEditor.Content.GUI
if (_dragOverItems == null)
_dragOverItems = new DragItems(ValidateDragItem);
-
_dragOverItems.OnDragEnter(data);
var result = GetDragEffect(data);
_validDragOver = result != DragDropEffect.None;
@@ -122,4 +122,70 @@ namespace FlaxEditor.Content.GUI
return result;
}
}
+
+ sealed class ContentNavigationSeparator : ComboBox
+ {
+ public ContentNavigationButton Target;
+
+ public ContentNavigationSeparator(ContentNavigationButton target, float x, float y, float height)
+ {
+ Target = target;
+ Bounds = new Rectangle(x, y, 16, height);
+ Offsets = new Margin(Bounds.X, Bounds.Width, Bounds.Y, Bounds.Height);
+ UpdateTransform();
+
+ MaximumItemsInViewCount = 20;
+ var style = Style.Current;
+ BackgroundColor = style.BackgroundNormal;
+ BackgroundColorHighlighted = BackgroundColor;
+ BackgroundColorSelected = BackgroundColor;
+ }
+
+ protected override ContextMenu OnCreatePopup()
+ {
+ // Update items
+ ClearItems();
+ foreach (var child in Target.TargetNode.Children)
+ {
+ if (child is ContentTreeNode node)
+ {
+ if (node.Folder.VisibleInHierarchy) // Respect the filter set by ContentFilterConfig.Filter(...)
+ AddItem(node.Folder.ShortName);
+ }
+ }
+
+ return base.OnCreatePopup();
+ }
+
+ public override void Draw()
+ {
+ var style = Style.Current;
+ var rect = new Rectangle(Float2.Zero, Size);
+ var color = IsDragOver ? style.BackgroundSelected * 0.6f : (_mouseDown ? style.BackgroundSelected : (IsMouseOver ? style.BackgroundHighlighted : Color.Transparent));
+ Render2D.FillRectangle(rect, color);
+ Render2D.DrawSprite(Editor.Instance.Icons.ArrowRight12, new Rectangle(rect.Location.X, rect.Y + rect.Size.Y * 0.25f, rect.Size.X, rect.Size.X), EnabledInHierarchy ? style.Foreground : style.ForegroundDisabled);
+ }
+
+ protected override void OnLayoutMenuButton(ContextMenuButton button, int index, bool construct = false)
+ {
+ button.Icon = Editor.Instance.Icons.FolderClosed32;
+ if (_tooltips != null && _tooltips.Length > index)
+ button.TooltipText = _tooltips[index];
+ }
+
+ protected override void OnItemClicked(int index)
+ {
+ base.OnItemClicked(index);
+
+ var item = _items[index];
+ foreach (var child in Target.TargetNode.Children)
+ {
+ if (child is ContentTreeNode node && node.Folder.ShortName == item)
+ {
+ Editor.Instance.Windows.ContentWin.Navigate(node);
+ return;
+ }
+ }
+ }
+ }
}
diff --git a/Source/Editor/Content/GUI/ContentView.cs b/Source/Editor/Content/GUI/ContentView.cs
index 6065ca9f8..5e054e5c6 100644
--- a/Source/Editor/Content/GUI/ContentView.cs
+++ b/Source/Editor/Content/GUI/ContentView.cs
@@ -261,11 +261,14 @@ namespace FlaxEditor.Content.GUI
ClearItems();
// Add references and link items
- _items.AddRange(items);
for (int i = 0; i < items.Count; i++)
{
- items[i].Parent = this;
- items[i].AddReference(this);
+ if (items[i].Visible)
+ {
+ items[i].Parent = this;
+ items[i].AddReference(this);
+ _items.Add(items[i]);
+ }
}
if (selection != null)
{
@@ -711,7 +714,7 @@ namespace FlaxEditor.Content.GUI
protected override void PerformLayoutBeforeChildren()
{
float width = GetClientArea().Width;
- float x = 0, y = 0;
+ float x = 0, y = 1;
float viewScale = _viewScale * 0.97f;
switch (ViewType)
@@ -722,21 +725,22 @@ namespace FlaxEditor.Content.GUI
int itemsToFit = Mathf.FloorToInt(width / defaultItemsWidth) - 1;
if (itemsToFit < 1)
itemsToFit = 1;
- float itemsWidth = width / Mathf.Max(itemsToFit, 1);
+ int xSpace = 4;
+ float itemsWidth = width / Mathf.Max(itemsToFit, 1) - xSpace;
float itemsHeight = itemsWidth / defaultItemsWidth * (ContentItem.DefaultHeight * viewScale);
var flooredItemsWidth = Mathf.Floor(itemsWidth);
var flooredItemsHeight = Mathf.Floor(itemsHeight);
- x = itemsToFit == 1 ? 0 : itemsWidth / itemsToFit;
+ x = itemsToFit == 1 ? 1 : itemsWidth / itemsToFit + xSpace;
for (int i = 0; i < _children.Count; i++)
{
var c = _children[i];
c.Bounds = new Rectangle(Mathf.Floor(x), Mathf.Floor(y), flooredItemsWidth, flooredItemsHeight);
- x += itemsWidth + itemsWidth / itemsToFit;
+ x += (itemsWidth + xSpace) + (itemsWidth + xSpace) / itemsToFit;
if (x + itemsWidth > width)
{
- x = itemsToFit == 1 ? 0 : itemsWidth / itemsToFit;
- y += itemsHeight + 5;
+ x = itemsToFit == 1 ? 1 : itemsWidth / itemsToFit + xSpace;
+ y += itemsHeight + 7;
}
}
if (x > 0)
@@ -751,7 +755,7 @@ namespace FlaxEditor.Content.GUI
{
var c = _children[i];
c.Bounds = new Rectangle(x, y, width, itemsHeight);
- y += itemsHeight + 5;
+ y += itemsHeight + 1;
}
y += 40.0f;
diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs
index 66825fb42..94db3a5b9 100644
--- a/Source/Editor/Content/Items/ContentItem.cs
+++ b/Source/Editor/Content/Items/ContentItem.cs
@@ -441,6 +441,9 @@ namespace FlaxEditor.Content
{
get
{
+ // Skip when hidden
+ if (!Visible)
+ return Rectangle.Empty;
var view = Parent as ContentView;
var size = Size;
switch (view?.ViewType ?? ContentViewType.Tiles)
@@ -483,6 +486,30 @@ namespace FlaxEditor.Content
else
Render2D.FillRectangle(rectangle, Color.Black);
}
+
+ ///
+ /// Draws the item thumbnail.
+ ///
+ /// The thumbnail rectangle.
+ /// /// Whether or not to draw the shadow. Overrides DrawShadow.
+ public void DrawThumbnail(ref Rectangle rectangle, bool shadow)
+ {
+ // Draw shadow
+ if (shadow)
+ {
+ const float thumbnailInShadowSize = 50.0f;
+ var shadowRect = rectangle.MakeExpanded((DefaultThumbnailSize - thumbnailInShadowSize) * rectangle.Width / DefaultThumbnailSize * 1.3f);
+ if (!_shadowIcon.IsValid)
+ _shadowIcon = Editor.Instance.Icons.AssetShadow128;
+ Render2D.DrawSprite(_shadowIcon, shadowRect);
+ }
+
+ // Draw thumbnail
+ if (_thumbnail.IsValid)
+ Render2D.DrawSprite(_thumbnail, rectangle);
+ else
+ Render2D.FillRectangle(rectangle, Color.Black);
+ }
///
/// Gets the amount of references to that item.
@@ -642,7 +669,6 @@ namespace FlaxEditor.Content
///
public override void Draw()
{
- // Cache data
var size = Size;
var style = Style.Current;
var view = Parent as ContentView;
@@ -655,9 +681,51 @@ namespace FlaxEditor.Content
{
case ContentViewType.Tiles:
{
- var thumbnailSize = size.X - 2 * DefaultMarginSize;
- thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize);
+ var thumbnailSize = size.X;
+ thumbnailRect = new Rectangle(0, 0, thumbnailSize, thumbnailSize);
nameAlignment = TextAlignment.Center;
+
+ if (this is ContentFolder)
+ {
+ // Small shadow
+ var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1);
+ var color = Color.Black.AlphaMultiplied(0.2f);
+ Render2D.FillRectangle(shadowRect, color);
+ Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f));
+
+ if (isSelected)
+ Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
+ else if (IsMouseOver)
+ Render2D.FillRectangle(clientRect, style.BackgroundHighlighted);
+
+ DrawThumbnail(ref thumbnailRect, false);
+ }
+ else
+ {
+ // Small shadow
+ var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1);
+ var color = Color.Black.AlphaMultiplied(0.2f);
+ Render2D.FillRectangle(shadowRect, color);
+
+ Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f));
+ Render2D.FillRectangle(TextRectangle, style.LightBackground);
+
+ var accentHeight = 2 * view.ViewScale;
+ var barRect = new Rectangle(0, thumbnailRect.Height - accentHeight, clientRect.Width, accentHeight);
+ Render2D.FillRectangle(barRect, Color.DimGray);
+
+ DrawThumbnail(ref thumbnailRect, false);
+ if (isSelected)
+ {
+ Render2D.FillRectangle(textRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
+ Render2D.DrawRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
+ }
+ else if (IsMouseOver)
+ {
+ Render2D.FillRectangle(textRect, style.BackgroundHighlighted);
+ Render2D.DrawRectangle(clientRect, style.BackgroundHighlighted);
+ }
+ }
break;
}
case ContentViewType.List:
@@ -665,23 +733,21 @@ namespace FlaxEditor.Content
var thumbnailSize = size.Y - 2 * DefaultMarginSize;
thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize);
nameAlignment = TextAlignment.Near;
+
+ if (isSelected)
+ Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
+ else if (IsMouseOver)
+ Render2D.FillRectangle(clientRect, style.BackgroundHighlighted);
+
+ DrawThumbnail(ref thumbnailRect);
break;
}
default: throw new ArgumentOutOfRangeException();
}
-
- // Draw background
- if (isSelected)
- Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
- else if (IsMouseOver)
- Render2D.FillRectangle(clientRect, style.BackgroundHighlighted);
-
- // Draw preview
- DrawThumbnail(ref thumbnailRect);
-
+
// Draw short name
Render2D.PushClip(ref textRect);
- Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 0.75f, 0.95f);
+ Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 1f, 0.95f);
Render2D.PopClip();
}
diff --git a/Source/Editor/Content/Items/ScriptItem.cs b/Source/Editor/Content/Items/ScriptItem.cs
index 811afc179..95b879b79 100644
--- a/Source/Editor/Content/Items/ScriptItem.cs
+++ b/Source/Editor/Content/Items/ScriptItem.cs
@@ -30,7 +30,7 @@ namespace FlaxEditor.Content
ShowFileExtension = true;
}
- private static string FilterScriptName(string input)
+ internal static string FilterScriptName(string input)
{
var length = input.Length;
var sb = new StringBuilder(length);
diff --git a/Source/Editor/Content/Tree/RootContentTreeNode.cs b/Source/Editor/Content/Tree/RootContentTreeNode.cs
index 6dde4cbb2..328c51874 100644
--- a/Source/Editor/Content/Tree/RootContentTreeNode.cs
+++ b/Source/Editor/Content/Tree/RootContentTreeNode.cs
@@ -17,6 +17,6 @@ namespace FlaxEditor.Content
}
///
- public override string NavButtonLabel => string.Empty;
+ public override string NavButtonLabel => " /";
}
}
diff --git a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs
index 877342154..03d58ef33 100644
--- a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs
+++ b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs
@@ -1,9 +1,12 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
-using FlaxEditor.Actions;
-using FlaxEditor.SceneGraph.Actors;
+using System;
+using System.Collections.Generic;
using FlaxEngine;
-using FlaxEngine.GUI;
+using FlaxEditor.Actions;
+using FlaxEditor.SceneGraph;
+using FlaxEditor.SceneGraph.Actors;
+using FlaxEditor.GUI.Tabs;
namespace FlaxEditor.CustomEditors.Dedicated
{
@@ -14,54 +17,853 @@ namespace FlaxEditor.CustomEditors.Dedicated
[CustomEditor(typeof(Spline)), DefaultEditor]
public class SplineEditor : ActorEditor
{
+ ///
+ /// Storage undo spline data
+ ///
+ private struct UndoData
+ {
+ public Spline Spline;
+ public BezierCurve.Keyframe[] BeforeKeyframes;
+ }
+
+ ///
+ /// Basis for creating tangent manipulation types for bezier curves.
+ ///
+ private class EditTangentOptionBase
+ {
+ ///
+ /// Called when user set selected tangent mode.
+ ///
+ /// Current spline selected on editor viewport.
+ /// Index of current keyframe selected on spline.
+ public virtual void OnSetMode(Spline spline, int index)
+ {
+ }
+
+ ///
+ /// Called when user select a keyframe (spline point) of current selected spline on editor viewport.
+ ///
+ /// Current spline selected on editor viewport.
+ /// Index of current keyframe selected on spline.
+ public virtual void OnSelectKeyframe(Spline spline, int index)
+ {
+ }
+
+ ///
+ /// Called when user select a tangent of current keyframe selected from spline.
+ ///
+ /// Current spline selected on editor viewport.
+ /// Index of current keyframe selected on spline.
+ public virtual void OnSelectTangent(Spline spline, int index)
+ {
+ }
+
+ ///
+ /// Called when the tangent in from current keyframe selected from spline is moved on editor viewport.
+ ///
+ /// Current spline selected on editor viewport.
+ /// Index of current keyframe selected on spline.
+ public virtual void OnMoveTangentIn(Spline spline, int index)
+ {
+ }
+
+ ///
+ /// Called when the tangent out from current keyframe selected from spline is moved on editor viewport.
+ ///
+ /// Current spline selected on editor viewport.
+ /// Current spline selected on editor viewport.
+ public virtual void OnMoveTangentOut(Spline spline, int index)
+ {
+ }
+ }
+
+ ///
+ /// Edit curve options manipulate the curve as free mode
+ ///
+ private sealed class FreeTangentMode : EditTangentOptionBase
+ {
+ ///
+ public override void OnSetMode(Spline spline, int index)
+ {
+ if (IsLinearTangentMode(spline, index) || IsSmoothInTangentMode(spline, index) || IsSmoothOutTangentMode(spline, index))
+ {
+ SetPointSmooth(spline, index);
+ }
+ }
+ }
+
+ ///
+ /// Edit curve options to set tangents to linear
+ ///
+ private sealed class LinearTangentMode : EditTangentOptionBase
+ {
+ ///
+ public override void OnSetMode(Spline spline, int index)
+ {
+ SetKeyframeLinear(spline, index);
+
+ // change the selection to tangent parent (a spline point / keyframe)
+ SetSelectSplinePointNode(spline, index);
+ }
+ }
+
+ ///
+ /// Edit curve options to align tangents of selected spline
+ ///
+ private sealed class AlignedTangentMode : EditTangentOptionBase
+ {
+ ///
+ public override void OnSetMode(Spline spline, int index)
+ {
+ SmoothIfNotAligned(spline, index);
+ }
+
+ ///
+ public override void OnSelectKeyframe(Spline spline, int index)
+ {
+ SmoothIfNotAligned(spline, index);
+ }
+
+ ///
+ public override void OnMoveTangentIn(Spline spline, int index)
+ {
+ SetPointAligned(spline, index, true);
+ }
+
+ ///
+ public override void OnMoveTangentOut(Spline spline, int index)
+ {
+ SetPointAligned(spline, index, false);
+ }
+
+ private void SmoothIfNotAligned(Spline spline, int index)
+ {
+ if (!IsAlignedTangentMode(spline, index))
+ {
+ SetPointSmooth(spline, index);
+ }
+ }
+
+ private void SetPointAligned(Spline spline, int index, bool alignWithIn)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ var referenceTangent = alignWithIn ? keyframe.TangentIn : keyframe.TangentOut;
+ var otherTangent = !alignWithIn ? keyframe.TangentIn : keyframe.TangentOut;
+
+ // inverse of reference tangent
+ otherTangent.Translation = -referenceTangent.Translation.Normalized * otherTangent.Translation.Length;
+
+ if (alignWithIn)
+ keyframe.TangentOut = otherTangent;
+ if (!alignWithIn)
+ keyframe.TangentIn = otherTangent;
+
+ spline.SetSplineKeyframe(index, keyframe);
+ }
+ }
+
+ ///
+ /// Edit curve options manipulate the curve setting selected point
+ /// tangent in as smoothed but tangent out as linear
+ ///
+ private sealed class SmoothInTangentMode : EditTangentOptionBase
+ {
+ ///
+ public override void OnSetMode(Spline spline, int index)
+ {
+ SetTangentSmoothIn(spline, index);
+ SetSelectTangentIn(spline, index);
+ }
+ }
+
+ ///
+ /// Edit curve options manipulate the curve setting selected point
+ /// tangent in as linear but tangent out as smoothed
+ ///
+ private sealed class SmoothOutTangentMode : EditTangentOptionBase
+ {
+ ///
+ public override void OnSetMode(Spline spline, int index)
+ {
+ SetTangentSmoothOut(spline, index);
+ SetSelectTangentOut(spline, index);
+ }
+ }
+
+ private sealed class IconTab : Tab
+ {
+ private sealed class IconTabHeader : Tabs.TabHeader
+ {
+ public IconTabHeader(Tabs tabs, Tab tab)
+ : base(tabs, tab)
+ {
+ }
+
+ public override bool OnMouseUp(Float2 location, MouseButton button)
+ {
+ if (EnabledInHierarchy && Tab.Enabled)
+ ((IconTab)Tab)._action();
+ return true;
+ }
+
+ public override void Draw()
+ {
+ base.Draw();
+
+ var tab = (IconTab)Tab;
+ var enabled = EnabledInHierarchy && tab.EnabledInHierarchy;
+ var style = FlaxEngine.GUI.Style.Current;
+ var size = Size;
+ var textHeight = 16.0f;
+ var iconSize = size.Y - textHeight;
+ var iconRect = new Rectangle((Width - iconSize) / 2, 0, iconSize, iconSize);
+ if (tab._mirrorIcon)
+ {
+ iconRect.Location.X += iconRect.Size.X;
+ iconRect.Size.X *= -1;
+ }
+ var color = style.Foreground;
+ if (!enabled)
+ color *= 0.6f;
+ Render2D.DrawSprite(tab._customIcon, iconRect, color);
+ Render2D.DrawText(style.FontMedium, tab._customText, new Rectangle(0, iconSize, size.X, textHeight), color, TextAlignment.Center, TextAlignment.Center);
+ }
+ }
+
+ private readonly Action _action;
+ private readonly string _customText;
+ private readonly SpriteHandle _customIcon;
+ private readonly bool _mirrorIcon;
+
+ public IconTab(Action action, string text, SpriteHandle icon, bool mirrorIcon = false)
+ : base(string.Empty, SpriteHandle.Invalid)
+ {
+ _action = action;
+ _customText = text;
+ _customIcon = icon;
+ _mirrorIcon = mirrorIcon;
+ }
+
+ public override Tabs.TabHeader CreateHeader()
+ {
+ return new IconTabHeader((Tabs)Parent, this);
+ }
+ }
+
+ private EditTangentOptionBase _currentTangentMode;
+
+ private Tabs _selectedPointsTabs, _allPointsTabs;
+ private Tab _freeTangentTab;
+ private Tab _linearTangentTab;
+ private Tab _alignedTangentTab;
+ private Tab _smoothInTangentTab;
+ private Tab _smoothOutTangentTab;
+ private Tab _setLinearAllTangentsTab;
+ private Tab _setSmoothAllTangentsTab;
+
+ private bool _tanInChanged;
+ private bool _tanOutChanged;
+ private Vector3 _lastTanInPos;
+ private Vector3 _lastTanOutPos;
+ private Spline _selectedSpline;
+ private SplineNode.SplinePointNode _selectedPoint;
+ private SplineNode.SplinePointNode _lastPointSelected;
+ private SplineNode.SplinePointTangentNode _selectedTangentIn;
+ private SplineNode.SplinePointTangentNode _selectedTangentOut;
+
+ private UndoData[] _selectedSplinesUndoData;
+
+ private bool HasPointSelected => _selectedPoint != null;
+ private bool HasTangentsSelected => _selectedTangentIn != null || _selectedTangentOut != null;
+
///
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
- if (Values.HasDifferentTypes == false)
+ _currentTangentMode = new FreeTangentMode();
+ if (Values.HasDifferentTypes || !(Values[0] is Spline spline))
+ return;
+ _selectedSpline = spline;
+
+ layout.Space(10);
+ var tabSize = 46;
+ var icons = Editor.Instance.Icons;
+
+ layout.Header("Selected spline point");
+ _selectedPointsTabs = new Tabs
{
- layout.Space(10);
- var grid = layout.CustomContainer();
- grid.CustomControl.SlotsHorizontally = 2;
- grid.CustomControl.SlotsVertically = 1;
- grid.Button("Set Linear Tangents").Button.Clicked += OnSetTangentsLinear;
- grid.Button("Set Smooth Tangents").Button.Clicked += OnSetTangentsSmooth;
+ Height = tabSize,
+ TabsSize = new Float2(tabSize),
+ AutoTabsSize = true,
+ Parent = layout.ContainerControl,
+ };
+ _linearTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedLinear, "Linear", icons.SplineLinear64));
+ _freeTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedFree, "Free", icons.SplineFree64));
+ _alignedTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedAligned, "Aligned", icons.SplineAligned64));
+ _smoothInTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedSmoothIn, "Smooth In", icons.SplineSmoothIn64));
+ _smoothOutTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedSmoothOut, "Smooth Out", icons.SplineSmoothIn64, true));
+ _selectedPointsTabs.SelectedTabIndex = -1;
+
+ layout.Header("All spline points");
+ _allPointsTabs = new Tabs
+ {
+ Height = tabSize,
+ TabsSize = new Float2(tabSize),
+ AutoTabsSize = true,
+ Parent = layout.ContainerControl,
+ };
+ _setLinearAllTangentsTab = _allPointsTabs.AddTab(new IconTab(OnSetTangentsLinear, "Set Linear Tangents", icons.SplineLinear64));
+ _setSmoothAllTangentsTab = _allPointsTabs.AddTab(new IconTab(OnSetTangentsSmooth, "Set Smooth Tangents", icons.SplineAligned64));
+ _allPointsTabs.SelectedTabIndex = -1;
+
+ if (_selectedSpline)
+ _selectedSpline.SplineUpdated += OnSplineEdited;
+
+ SetSelectedTangentTypeAsCurrent();
+ UpdateEditTabsSelection();
+ UpdateButtonsEnabled();
+ }
+
+ ///
+ protected override void Deinitialize()
+ {
+ if (_selectedSpline)
+ _selectedSpline.SplineUpdated -= OnSplineEdited;
+ }
+
+ private void OnSplineEdited()
+ {
+ UpdateEditTabsSelection();
+ UpdateButtonsEnabled();
+ }
+
+ ///
+ public override void Refresh()
+ {
+ base.Refresh();
+
+ UpdateSelectedPoint();
+ UpdateSelectedTangent();
+
+ if (!CanEditTangent())
+ return;
+
+ var index = _lastPointSelected.Index;
+ var currentTangentInPosition = _selectedSpline.GetSplineLocalTangent(index, true).Translation;
+ var currentTangentOutPosition = _selectedSpline.GetSplineLocalTangent(index, false).Translation;
+
+ if (_selectedTangentIn != null)
+ {
+ _tanInChanged = _lastTanInPos != currentTangentInPosition;
+ _lastTanInPos = currentTangentInPosition;
}
+
+ if (_selectedTangentOut != null)
+ {
+ _tanOutChanged = _lastTanOutPos != currentTangentOutPosition;
+ _lastTanOutPos = currentTangentOutPosition;
+ }
+
+ if (_tanInChanged)
+ _currentTangentMode.OnMoveTangentIn(_selectedSpline, index);
+ if (_tanOutChanged)
+ _currentTangentMode.OnMoveTangentOut(_selectedSpline, index);
+
+ currentTangentInPosition = _selectedSpline.GetSplineLocalTangent(index, true).Translation;
+ currentTangentOutPosition = _selectedSpline.GetSplineLocalTangent(index, false).Translation;
+
+ // Update last tangents position after changes
+ if (_selectedSpline)
+ _lastTanInPos = currentTangentInPosition;
+ if (_selectedSpline)
+ _lastTanOutPos = currentTangentOutPosition;
+ _tanInChanged = false;
+ _tanOutChanged = false;
+ }
+
+ private void SetSelectedTangentTypeAsCurrent()
+ {
+ if (_lastPointSelected == null || _selectedPoint == null)
+ return;
+ if (IsLinearTangentMode(_selectedSpline, _lastPointSelected.Index))
+ SetModeLinear();
+ else if (IsAlignedTangentMode(_selectedSpline, _lastPointSelected.Index))
+ SetModeAligned();
+ else if (IsSmoothInTangentMode(_selectedSpline, _lastPointSelected.Index))
+ SetModeSmoothIn();
+ else if (IsSmoothOutTangentMode(_selectedSpline, _lastPointSelected.Index))
+ SetModeSmoothOut();
+ else if (IsFreeTangentMode(_selectedSpline, _lastPointSelected.Index))
+ SetModeFree();
+ }
+
+ private void UpdateEditTabsSelection()
+ {
+ _selectedPointsTabs.Enabled = CanEditTangent();
+ if (!_selectedPointsTabs.Enabled)
+ {
+ _selectedPointsTabs.SelectedTabIndex = -1;
+ return;
+ }
+
+ var isFree = _currentTangentMode is FreeTangentMode;
+ var isLinear = _currentTangentMode is LinearTangentMode;
+ var isAligned = _currentTangentMode is AlignedTangentMode;
+ var isSmoothIn = _currentTangentMode is SmoothInTangentMode;
+ var isSmoothOut = _currentTangentMode is SmoothOutTangentMode;
+
+ if (isFree)
+ _selectedPointsTabs.SelectedTab = _freeTangentTab;
+ else if (isLinear)
+ _selectedPointsTabs.SelectedTab = _linearTangentTab;
+ else if (isAligned)
+ _selectedPointsTabs.SelectedTab = _alignedTangentTab;
+ else if (isSmoothIn)
+ _selectedPointsTabs.SelectedTab = _smoothInTangentTab;
+ else if (isSmoothOut)
+ _selectedPointsTabs.SelectedTab = _smoothOutTangentTab;
+ else
+ _selectedPointsTabs.SelectedTabIndex = -1;
+ }
+
+ private void UpdateButtonsEnabled()
+ {
+ _linearTangentTab.Enabled = CanEditTangent();
+ _freeTangentTab.Enabled = CanEditTangent();
+ _alignedTangentTab.Enabled = CanEditTangent();
+ _smoothInTangentTab.Enabled = CanSetTangentSmoothIn();
+ _smoothOutTangentTab.Enabled = CanSetTangentSmoothOut();
+ _setLinearAllTangentsTab.Enabled = CanSetAllTangentsLinear();
+ _setSmoothAllTangentsTab.Enabled = CanSetAllTangentsSmooth();
+ }
+
+ private bool CanEditTangent()
+ {
+ return !HasDifferentTypes && !HasDifferentValues && (HasPointSelected || HasTangentsSelected);
+ }
+
+ private bool CanSetTangentSmoothIn()
+ {
+ if (!CanEditTangent())
+ return false;
+ return _lastPointSelected.Index != 0;
+ }
+
+ private bool CanSetTangentSmoothOut()
+ {
+ if (!CanEditTangent())
+ return false;
+ return _lastPointSelected.Index < _selectedSpline.SplinePointsCount - 1;
+ }
+
+ private bool CanSetAllTangentsSmooth()
+ {
+ return _selectedSpline != null;
+ }
+
+ private bool CanSetAllTangentsLinear()
+ {
+ return _selectedSpline != null;
+ }
+
+ private void SetModeLinear()
+ {
+ if (_currentTangentMode is LinearTangentMode)
+ return;
+ _currentTangentMode = new LinearTangentMode();
+ _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index);
+ }
+
+ private void SetModeFree()
+ {
+ if (_currentTangentMode is FreeTangentMode)
+ return;
+ _currentTangentMode = new FreeTangentMode();
+ _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index);
+ }
+
+ private void SetModeAligned()
+ {
+ if (_currentTangentMode is AlignedTangentMode)
+ return;
+ _currentTangentMode = new AlignedTangentMode();
+ _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index);
+ }
+
+ private void SetModeSmoothIn()
+ {
+ if (_currentTangentMode is SmoothInTangentMode)
+ return;
+ _currentTangentMode = new SmoothInTangentMode();
+ _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index);
+ }
+
+ private void SetModeSmoothOut()
+ {
+ if (_currentTangentMode is SmoothOutTangentMode)
+ return;
+ _currentTangentMode = new SmoothOutTangentMode();
+ _currentTangentMode.OnSetMode(_selectedSpline, _lastPointSelected.Index);
+ }
+
+ private void UpdateSelectedPoint()
+ {
+ // works only if select one spline
+ if (_selectedSpline)
+ {
+ var currentSelected = Editor.Instance.SceneEditing.Selection[0];
+
+ if (currentSelected == _selectedPoint)
+ return;
+ if (currentSelected is SplineNode.SplinePointNode selectedPoint)
+ {
+ _selectedPoint = selectedPoint;
+ _lastPointSelected = _selectedPoint;
+ _currentTangentMode.OnSelectKeyframe(_selectedSpline, _lastPointSelected.Index);
+ }
+ else
+ {
+ _selectedPoint = null;
+ }
+ }
+ else
+ {
+ _selectedPoint = null;
+ }
+
+ SetSelectedTangentTypeAsCurrent();
+ UpdateEditTabsSelection();
+ UpdateButtonsEnabled();
+ }
+
+ private void UpdateSelectedTangent()
+ {
+ // works only if select one spline
+ if (_lastPointSelected == null || Editor.Instance.SceneEditing.SelectionCount != 1)
+ {
+ _selectedTangentIn = null;
+ _selectedTangentOut = null;
+ return;
+ }
+
+ var currentSelected = Editor.Instance.SceneEditing.Selection[0];
+
+ if (currentSelected is not SplineNode.SplinePointTangentNode selectedPoint)
+ {
+ _selectedTangentIn = null;
+ _selectedTangentOut = null;
+ return;
+ }
+
+ if (currentSelected == _selectedTangentIn)
+ return;
+ if (currentSelected == _selectedTangentOut)
+ return;
+
+ var index = _lastPointSelected.Index;
+
+ if (currentSelected.Transform == _selectedSpline.GetSplineTangent(index, true))
+ {
+ _selectedTangentIn = selectedPoint;
+ _selectedTangentOut = null;
+ _currentTangentMode.OnSelectTangent(_selectedSpline, index);
+
+ return;
+ }
+
+ if (currentSelected.Transform == _selectedSpline.GetSplineTangent(index, false))
+ {
+ _selectedTangentOut = selectedPoint;
+ _selectedTangentIn = null;
+ _currentTangentMode.OnSelectTangent(_selectedSpline, index);
+ return;
+ }
+
+ _selectedTangentIn = null;
+ _selectedTangentOut = null;
+ }
+
+ private void StartEditSpline()
+ {
+ if (Presenter.Undo != null && Presenter.Undo.Enabled)
+ {
+ // Capture 'before' state for undo
+ var splines = new List();
+ for (int i = 0; i < Values.Count; i++)
+ {
+ if (Values[i] is Spline spline)
+ {
+ splines.Add(new UndoData
+ {
+ Spline = spline,
+ BeforeKeyframes = spline.SplineKeyframes.Clone() as BezierCurve.Keyframe[]
+ });
+ }
+ }
+ _selectedSplinesUndoData = splines.ToArray();
+ }
+ }
+
+ private void EndEditSpline()
+ {
+ // Update buttons state
+ UpdateEditTabsSelection();
+
+ if (Presenter.Undo != null && Presenter.Undo.Enabled)
+ {
+ // Add undo
+ foreach (var splineUndoData in _selectedSplinesUndoData)
+ {
+ Presenter.Undo.AddAction(new EditSplineAction(_selectedSpline, splineUndoData.BeforeKeyframes));
+ SplineNode.OnSplineEdited(splineUndoData.Spline);
+ Editor.Instance.Scene.MarkSceneEdited(splineUndoData.Spline.Scene);
+ }
+ }
+ }
+
+ private void OnSetSelectedLinear()
+ {
+ StartEditSpline();
+ SetModeLinear();
+ EndEditSpline();
+ }
+
+ private void OnSetSelectedFree()
+ {
+ StartEditSpline();
+ SetModeFree();
+ EndEditSpline();
+ }
+
+ private void OnSetSelectedAligned()
+ {
+ StartEditSpline();
+ SetModeAligned();
+ EndEditSpline();
+ }
+
+ private void OnSetSelectedSmoothIn()
+ {
+ StartEditSpline();
+ SetModeSmoothIn();
+ EndEditSpline();
+ }
+
+ private void OnSetSelectedSmoothOut()
+ {
+ StartEditSpline();
+ SetModeSmoothOut();
+ EndEditSpline();
}
private void OnSetTangentsLinear()
{
- var enableUndo = Presenter.Undo != null && Presenter.Undo.Enabled;
- for (int i = 0; i < Values.Count; i++)
- {
- if (Values[i] is Spline spline)
- {
- var before = enableUndo ? (BezierCurve.Keyframe[])spline.SplineKeyframes.Clone() : null;
- spline.SetTangentsLinear();
- if (enableUndo)
- Presenter.Undo.AddAction(new EditSplineAction(spline, before));
- SplineNode.OnSplineEdited(spline);
- Editor.Instance.Scene.MarkSceneEdited(spline.Scene);
- }
- }
+ StartEditSpline();
+ _selectedSpline.SetTangentsLinear();
+ _selectedSpline.UpdateSpline();
+ EndEditSpline();
}
private void OnSetTangentsSmooth()
{
- var enableUndo = Presenter.Undo != null && Presenter.Undo.Enabled;
- for (int i = 0; i < Values.Count; i++)
+ StartEditSpline();
+ _selectedSpline.SetTangentsSmooth();
+ _selectedSpline.UpdateSpline();
+ EndEditSpline();
+ }
+
+ private static bool IsFreeTangentMode(Spline spline, int index)
+ {
+ if (IsLinearTangentMode(spline, index) ||
+ IsAlignedTangentMode(spline, index) ||
+ IsSmoothInTangentMode(spline, index) ||
+ IsSmoothOutTangentMode(spline, index))
{
- if (Values[i] is Spline spline)
+ return false;
+ }
+ return true;
+ }
+
+ private static bool IsLinearTangentMode(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ return keyframe.TangentIn.Translation.Length == 0 && keyframe.TangentOut.Translation.Length == 0;
+ }
+
+ private static bool IsAlignedTangentMode(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ var tangentIn = keyframe.TangentIn.Translation;
+ var tangentOut = keyframe.TangentOut.Translation;
+ if (tangentIn.Length == 0 || tangentOut.Length == 0)
+ return false;
+
+ var angleBetweenTwoTangents = Vector3.Dot(tangentIn.Normalized, tangentOut.Normalized);
+ if (angleBetweenTwoTangents < -0.99f)
+ return true;
+
+ return false;
+ }
+
+ private static bool IsSmoothInTangentMode(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ return keyframe.TangentIn.Translation.Length > 0 && keyframe.TangentOut.Translation.Length == 0;
+ }
+
+ private static bool IsSmoothOutTangentMode(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ return keyframe.TangentOut.Translation.Length > 0 && keyframe.TangentIn.Translation.Length == 0;
+ }
+
+ private static void SetKeyframeLinear(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ keyframe.TangentIn.Translation = Vector3.Zero;
+ keyframe.TangentOut.Translation = Vector3.Zero;
+
+ var lastSplineIndex = spline.SplinePointsCount - 1;
+ if (index == lastSplineIndex && spline.IsLoop)
+ {
+ var lastPoint = spline.GetSplineKeyframe(lastSplineIndex);
+ lastPoint.TangentIn.Translation = Vector3.Zero;
+ lastPoint.TangentOut.Translation = Vector3.Zero;
+ spline.SetSplineKeyframe(lastSplineIndex, lastPoint);
+ }
+
+ spline.SetSplineKeyframe(index, keyframe);
+ spline.UpdateSpline();
+ }
+
+ private static void SetTangentSmoothIn(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+
+ // Auto smooth tangent if's linear
+ if (keyframe.TangentIn.Translation.Length == 0)
+ {
+ var smoothRange = SplineNode.NodeSizeByDistance(spline.GetSplineTangent(index, false).Translation, 10f);
+ var previousKeyframe = spline.GetSplineKeyframe(index - 1);
+ var tangentDirection = keyframe.Value.WorldToLocalVector(previousKeyframe.Value.Translation - keyframe.Value.Translation);
+ tangentDirection = tangentDirection.Normalized * smoothRange;
+ keyframe.TangentIn.Translation = tangentDirection;
+ }
+
+ keyframe.TangentOut.Translation = Vector3.Zero;
+ spline.SetSplineKeyframe(index, keyframe);
+ spline.UpdateSpline();
+ }
+
+ private static void SetTangentSmoothOut(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+
+ // Auto smooth tangent if's linear
+ if (keyframe.TangentOut.Translation.Length == 0)
+ {
+ var smoothRange = SplineNode.NodeSizeByDistance(spline.GetSplineTangent(index, false).Translation, 10f);
+ var nextKeyframe = spline.GetSplineKeyframe(index + 1);
+ var tangentDirection = keyframe.Value.WorldToLocalVector(nextKeyframe.Value.Translation - keyframe.Value.Translation);
+ tangentDirection = tangentDirection.Normalized * smoothRange;
+ keyframe.TangentOut.Translation = tangentDirection;
+ }
+
+ keyframe.TangentIn.Translation = Vector3.Zero;
+
+ spline.SetSplineKeyframe(index, keyframe);
+ spline.UpdateSpline();
+ }
+
+ private static void SetPointSmooth(Spline spline, int index)
+ {
+ var keyframe = spline.GetSplineKeyframe(index);
+ var tangentInSize = keyframe.TangentIn.Translation.Length;
+ var tangentOutSize = keyframe.TangentOut.Translation.Length;
+
+ var isLastKeyframe = index >= spline.SplinePointsCount - 1;
+ var isFirstKeyframe = index <= 0;
+ var smoothRange = SplineNode.NodeSizeByDistance(spline.GetSplinePoint(index), 10f);
+
+ // Force smooth it's linear point
+ if (tangentInSize == 0f)
+ tangentInSize = smoothRange;
+ if (tangentOutSize == 0f)
+ tangentOutSize = smoothRange;
+
+ // Try get next / last keyframe
+ var nextKeyframe = !isLastKeyframe ? spline.GetSplineKeyframe(index + 1) : keyframe;
+ var previousKeyframe = !isFirstKeyframe ? spline.GetSplineKeyframe(index - 1) : keyframe;
+
+ // calc form from Spline.cpp -> SetTangentsSmooth
+ // get tangent direction
+ var tangentDirection = (keyframe.Value.Translation - previousKeyframe.Value.Translation + nextKeyframe.Value.Translation - keyframe.Value.Translation).Normalized;
+
+ keyframe.TangentIn.Translation = -tangentDirection;
+ keyframe.TangentOut.Translation = tangentDirection;
+
+ keyframe.TangentIn.Translation *= tangentInSize;
+ keyframe.TangentOut.Translation *= tangentOutSize;
+
+ spline.SetSplineKeyframe(index, keyframe);
+ spline.UpdateSpline();
+ }
+
+ private static SplineNode.SplinePointNode GetSplinePointNode(Spline spline, int index)
+ {
+ return (SplineNode.SplinePointNode)SceneGraphFactory.FindNode(spline.ID).ChildNodes[index];
+ }
+
+ private static SplineNode.SplinePointTangentNode GetSplineTangentInNode(Spline spline, int index)
+ {
+ var point = GetSplinePointNode(spline, index);
+ var tangentIn = spline.GetSplineTangent(index, true);
+ var tangentNodes = point.ChildNodes;
+
+ // find tangent in node comparing all child nodes position
+ for (int i = 0; i < tangentNodes.Count; i++)
+ {
+ if (tangentNodes[i].Transform.Translation == tangentIn.Translation)
{
- var before = enableUndo ? (BezierCurve.Keyframe[])spline.SplineKeyframes.Clone() : null;
- spline.SetTangentsSmooth();
- if (enableUndo)
- Presenter.Undo.AddAction(new EditSplineAction(spline, before));
- SplineNode.OnSplineEdited(spline);
- Editor.Instance.Scene.MarkSceneEdited(spline.Scene);
+ return (SplineNode.SplinePointTangentNode)tangentNodes[i];
}
}
+
+ return null;
+ }
+
+ private static SplineNode.SplinePointTangentNode GetSplineTangentOutNode(Spline spline, int index)
+ {
+ var point = GetSplinePointNode(spline, index);
+ var tangentOut = spline.GetSplineTangent(index, false);
+ var tangentNodes = point.ChildNodes;
+
+ // find tangent out node comparing all child nodes position
+ for (int i = 0; i < tangentNodes.Count; i++)
+ {
+ if (tangentNodes[i].Transform.Translation == tangentOut.Translation)
+ {
+ return (SplineNode.SplinePointTangentNode)tangentNodes[i];
+ }
+ }
+
+ return null;
+ }
+
+ private static void SetSelectSplinePointNode(Spline spline, int index)
+ {
+ Editor.Instance.SceneEditing.Select(GetSplinePointNode(spline, index));
+ }
+
+ private static void SetSelectTangentIn(Spline spline, int index)
+ {
+ Editor.Instance.SceneEditing.Select(GetSplineTangentInNode(spline, index));
+ }
+
+ private static void SetSelectTangentOut(Spline spline, int index)
+ {
+ Editor.Instance.SceneEditing.Select(GetSplineTangentOutNode(spline, index));
}
}
}
diff --git a/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs b/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs
index d04b5b6da..e1a2b0195 100644
--- a/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs
@@ -123,9 +123,9 @@ namespace FlaxEditor.CustomEditors.Editors
{
if (tree.IsLayoutLocked)
return;
- root.LockChildrenRecursive();
+ tree.LockChildrenRecursive();
Utilities.Utils.UpdateSearchPopupFilter(root, searchBox.Text);
- root.UnlockChildrenRecursive();
+ tree.UnlockChildrenRecursive();
menu.PerformLayout();
};
root.ExpandAll(true);
diff --git a/Source/Editor/CustomEditors/Editors/InputEditor.cs b/Source/Editor/CustomEditors/Editors/InputEditor.cs
new file mode 100644
index 000000000..416626e81
--- /dev/null
+++ b/Source/Editor/CustomEditors/Editors/InputEditor.cs
@@ -0,0 +1,122 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System.Collections.Generic;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.CustomEditors.Editors
+{
+ ///
+ /// Default implementation of the inspector used to edit input event properties.
+ ///
+ [CustomEditor(typeof(InputEvent)), DefaultEditor]
+ public class InputEventEditor : CustomEditor
+ {
+ private Dropdown _dropdown;
+
+ ///
+ public override DisplayStyle Style => DisplayStyle.Inline;
+
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ var dropdownElement = layout.Custom();
+ _dropdown = dropdownElement.CustomControl;
+ var names = new List();
+ foreach (var mapping in Input.ActionMappings)
+ {
+ if (!names.Contains(mapping.Name))
+ names.Add(mapping.Name);
+ }
+ _dropdown.Items = names;
+ if (Values[0] is InputEvent inputEvent && names.Contains(inputEvent.Name))
+ _dropdown.SelectedItem = inputEvent.Name;
+ _dropdown.SelectedIndexChanged += OnSelectedIndexChanged;
+ }
+
+ private void OnSelectedIndexChanged(Dropdown dropdown)
+ {
+ SetValue(new InputEvent(dropdown.SelectedItem));
+ }
+
+ ///
+ public override void Refresh()
+ {
+ base.Refresh();
+
+ if (HasDifferentValues)
+ {
+ }
+ else
+ {
+ if (Values[0] is InputEvent inputEvent && _dropdown.Items.Contains(inputEvent.Name))
+ _dropdown.SelectedItem = inputEvent.Name;
+ }
+ }
+
+ ///
+ protected override void Deinitialize()
+ {
+ if (_dropdown != null)
+ _dropdown.SelectedIndexChanged -= OnSelectedIndexChanged;
+ _dropdown = null;
+ }
+ }
+
+ ///
+ /// Default implementation of the inspector used to edit input axis properties.
+ ///
+ [CustomEditor(typeof(InputAxis)), DefaultEditor]
+ public class InputAxisEditor : CustomEditor
+ {
+ private Dropdown _dropdown;
+
+ ///
+ public override DisplayStyle Style => DisplayStyle.Inline;
+
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ var dropdownElement = layout.Custom();
+ _dropdown = dropdownElement.CustomControl;
+ var names = new List();
+ foreach (var mapping in Input.AxisMappings)
+ {
+ if (!names.Contains(mapping.Name))
+ names.Add(mapping.Name);
+ }
+ _dropdown.Items = names;
+ if (Values[0] is InputAxis inputAxis && names.Contains(inputAxis.Name))
+ _dropdown.SelectedItem = inputAxis.Name;
+ _dropdown.SelectedIndexChanged += OnSelectedIndexChanged;
+ }
+
+ private void OnSelectedIndexChanged(Dropdown dropdown)
+ {
+ SetValue(new InputAxis(dropdown.SelectedItem));
+ }
+
+ ///
+ public override void Refresh()
+ {
+ base.Refresh();
+
+ if (HasDifferentValues)
+ {
+ }
+ else
+ {
+ if (Values[0] is InputAxis inputAxis && _dropdown.Items.Contains(inputAxis.Name))
+ _dropdown.SelectedItem = inputAxis.Name;
+ }
+ }
+
+ ///
+ protected override void Deinitialize()
+ {
+ if (_dropdown != null)
+ _dropdown.SelectedIndexChanged -= OnSelectedIndexChanged;
+ _dropdown = null;
+ }
+ }
+}
diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs
index 277f9ecb0..9607680f2 100644
--- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs
@@ -39,6 +39,8 @@ namespace FlaxEditor.CustomEditors.Editors
_entryIndex = entryIndex;
_modelInstance = modelInstance;
var slots = modelInstance.MaterialSlots;
+ if (slots == null || entryIndex >= slots.Length)
+ return;
if (entry.Material == slots[entryIndex].Material)
{
// Ensure that entry with default material set is set back to null
diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs
index ab5fd5d1a..3d2dd86aa 100644
--- a/Source/Editor/CustomEditors/Editors/TagEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs
@@ -593,9 +593,9 @@ namespace FlaxEditor.CustomEditors.Editors
{
if (tree.IsLayoutLocked)
return;
- root.LockChildrenRecursive();
+ tree.LockChildrenRecursive();
Utilities.Utils.UpdateSearchPopupFilter(root, searchBox.Text);
- root.UnlockChildrenRecursive();
+ tree.UnlockChildrenRecursive();
menu.PerformLayout();
};
diff --git a/Source/Editor/Editor.Build.cs b/Source/Editor/Editor.Build.cs
index a971dbefa..f497225bb 100644
--- a/Source/Editor/Editor.Build.cs
+++ b/Source/Editor/Editor.Build.cs
@@ -41,6 +41,7 @@ public class Editor : EditorModule
options.ScriptingAPI.SystemReferences.Add("System.Xml.ReaderWriter");
options.ScriptingAPI.SystemReferences.Add("System.Text.RegularExpressions");
options.ScriptingAPI.SystemReferences.Add("System.ComponentModel.TypeConverter");
+ options.ScriptingAPI.SystemReferences.Add("System.IO.Compression.ZipFile");
// Enable optimizations for Editor, disable this for debugging the editor
if (options.Configuration == TargetConfiguration.Development)
diff --git a/Source/Editor/EditorIcons.cs b/Source/Editor/EditorIcons.cs
index fb3c46a41..c16ec27cc 100644
--- a/Source/Editor/EditorIcons.cs
+++ b/Source/Editor/EditorIcons.cs
@@ -97,6 +97,10 @@ namespace FlaxEditor
public SpriteHandle Build64;
public SpriteHandle Add64;
public SpriteHandle ShipIt64;
+ public SpriteHandle SplineFree64;
+ public SpriteHandle SplineLinear64;
+ public SpriteHandle SplineAligned64;
+ public SpriteHandle SplineSmoothIn64;
// 96px
public SpriteHandle Toolbox96;
diff --git a/Source/Editor/GUI/ComboBox.cs b/Source/Editor/GUI/ComboBox.cs
index 2481316d0..da2106450 100644
--- a/Source/Editor/GUI/ComboBox.cs
+++ b/Source/Editor/GUI/ComboBox.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using FlaxEditor.GUI.ContextMenu;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -396,14 +397,24 @@ namespace FlaxEditor.GUI
_popupMenu.ButtonClicked += btn =>
{
OnItemClicked((int)btn.Tag);
- _popupMenu?.Hide();
+ if (SupportMultiSelect)
+ {
+ // Don't hide in multi-select, so user can edit multiple elements instead of just one
+ UpdateButtons();
+ _popupMenu?.PerformLayout();
+ }
+ else
+ {
+ _popupMenu?.Hide();
+ }
};
}
// Check if menu hs been already shown
if (_popupMenu.Visible)
{
- _popupMenu.Hide();
+ if (!SupportMultiSelect)
+ _popupMenu.Hide();
return;
}
@@ -412,27 +423,7 @@ namespace FlaxEditor.GUI
// Check if has any items
if (_items.Count > 0)
{
- // Setup items list
- var itemControls = _popupMenu.Items.ToArray();
- foreach (var e in itemControls)
- e.Dispose();
- if (Sorted)
- _items.Sort();
- var style = Style.Current;
- for (int i = 0; i < _items.Count; i++)
- {
- var btn = _popupMenu.AddButton(_items[i]);
- if (_selectedIndices.Contains(i))
- {
- btn.Icon = style.CheckBoxTick;
- }
- if (_tooltips != null && _tooltips.Length > i)
- {
- btn.TooltipText = _tooltips[i];
- }
-
- btn.Tag = i;
- }
+ UpdateButtons();
// Show dropdown list
_popupMenu.MinimumWidth = Width;
@@ -440,6 +431,54 @@ namespace FlaxEditor.GUI
}
}
+ ///
+ /// Updates buttons layout.
+ ///
+ private void UpdateButtons()
+ {
+ if (_popupMenu.Items.Count() != _items.Count)
+ {
+ var itemControls = _popupMenu.Items.ToArray();
+ foreach (var e in itemControls)
+ e.Dispose();
+ if (Sorted)
+ _items.Sort();
+ for (int i = 0; i < _items.Count; i++)
+ {
+ var btn = _popupMenu.AddButton(_items[i]);
+ OnLayoutMenuButton(btn, i, true);
+ btn.Tag = i;
+ }
+ }
+ else
+ {
+ var itemControls = _popupMenu.Items.ToArray();
+ if (Sorted)
+ _items.Sort();
+ for (int i = 0; i < _items.Count; i++)
+ {
+ if (itemControls[i] is ContextMenuButton btn)
+ {
+ btn.Text = _items[i];
+ OnLayoutMenuButton(btn, i, true);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Called when button is created or updated. Can be used to customize the visuals.
+ ///
+ /// The button.
+ /// The item index.
+ /// true if button is created else it is repainting the button
+ protected virtual void OnLayoutMenuButton(ContextMenuButton button, int index, bool construct = false)
+ {
+ button.Checked = _selectedIndices.Contains(index);
+ if (_tooltips != null && _tooltips.Length > index)
+ button.TooltipText = _tooltips[index];
+ }
+
///
/// Creates the popup menu.
///
@@ -460,6 +499,8 @@ namespace FlaxEditor.GUI
_popupMenu = null;
}
+ if (IsDisposing)
+ return;
_selectedIndices.Clear();
_selectedIndices = null;
_items.Clear();
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
index c2b130e4e..e371f7c4b 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
@@ -138,7 +138,7 @@ namespace FlaxEditor.GUI.ContextMenu
// Draw icon
const float iconSize = 14;
- var icon = Checked ? Style.Current.CheckBoxTick : Icon;
+ var icon = Checked ? style.CheckBoxTick : Icon;
if (icon.IsValid)
Render2D.DrawSprite(icon, new Rectangle(-iconSize - 1, (Height - iconSize) / 2, iconSize, iconSize), textColor);
}
diff --git a/Source/Editor/GUI/MainMenu.cs b/Source/Editor/GUI/MainMenu.cs
index 66b10f0aa..370ad056b 100644
--- a/Source/Editor/GUI/MainMenu.cs
+++ b/Source/Editor/GUI/MainMenu.cs
@@ -304,6 +304,17 @@ namespace FlaxEditor.GUI
return true;
}
+ ///
+ public override bool OnKeyDown(KeyboardKeys key)
+ {
+ if (base.OnKeyDown(key))
+ return true;
+
+ // Fallback to the edit window for shortcuts
+ var editor = Editor.Instance;
+ return editor.Windows.EditWin.InputActions.Process(editor, this, key);
+ }
+
///
protected override void PerformLayoutAfterChildren()
{
diff --git a/Source/Editor/GUI/NavigationBar.cs b/Source/Editor/GUI/NavigationBar.cs
index 3c717423b..b3811b4a9 100644
--- a/Source/Editor/GUI/NavigationBar.cs
+++ b/Source/Editor/GUI/NavigationBar.cs
@@ -50,14 +50,9 @@ namespace FlaxEditor.GUI
{
if (toolstrip == null)
return;
-
var lastToolstripButton = toolstrip.LastButton;
var parentSize = Parent.Size;
- Bounds = new Rectangle
- (
- new Float2(lastToolstripButton.Right + 8.0f, 0),
- new Float2(parentSize.X - X - 8.0f, toolstrip.Height)
- );
+ Bounds = new Rectangle(lastToolstripButton.Right + 8.0f, 0, parentSize.X - X - 8.0f, toolstrip.Height);
}
}
}
diff --git a/Source/Editor/GUI/Tabs/Tab.cs b/Source/Editor/GUI/Tabs/Tab.cs
index 67be0259b..3968cc17d 100644
--- a/Source/Editor/GUI/Tabs/Tab.cs
+++ b/Source/Editor/GUI/Tabs/Tab.cs
@@ -77,5 +77,14 @@ namespace FlaxEditor.GUI.Tabs
{
Deselected?.Invoke(this);
}
+
+ ///
+ /// Factory method for tabs header control (UI for the tabs strip to represent this tab).
+ ///
+ /// The tab header control.
+ public virtual Tabs.TabHeader CreateHeader()
+ {
+ return new Tabs.TabHeader((Tabs)Parent, this);
+ }
}
}
diff --git a/Source/Editor/GUI/Tabs/Tabs.cs b/Source/Editor/GUI/Tabs/Tabs.cs
index e01178c3d..f9bfd3f6c 100644
--- a/Source/Editor/GUI/Tabs/Tabs.cs
+++ b/Source/Editor/GUI/Tabs/Tabs.cs
@@ -24,11 +24,6 @@ namespace FlaxEditor.GUI.Tabs
///
protected Tabs Tabs;
- ///
- /// The index of the tab.
- ///
- protected int Index;
-
///
/// The tab.
///
@@ -38,21 +33,22 @@ namespace FlaxEditor.GUI.Tabs
/// Initializes a new instance of the class.
///
/// The tabs.
- /// The tab index.
/// The tab.
- public TabHeader(Tabs tabs, int index, Tab tab)
+ public TabHeader(Tabs tabs, Tab tab)
: base(Float2.Zero, tabs._tabsSize)
{
Tabs = tabs;
- Index = index;
Tab = tab;
}
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
- Tabs.SelectedTabIndex = Index;
- Tabs.Focus();
+ if (EnabledInHierarchy && Tab.Enabled)
+ {
+ Tabs.SelectedTab = Tab;
+ Tabs.Focus();
+ }
return true;
}
@@ -61,19 +57,20 @@ namespace FlaxEditor.GUI.Tabs
{
base.Draw();
- // Cache data
var style = Style.Current;
+ var enabled = EnabledInHierarchy && Tab.EnabledInHierarchy;
var tabRect = new Rectangle(Float2.Zero, Size);
- bool isTabSelected = Tabs._selectedIndex == Index;
- bool isMouseOverTab = IsMouseOver;
var textOffset = Tabs._orientation == Orientation.Horizontal ? 0 : 8;
// Draw bar
- if (isTabSelected)
+ if (Tabs.SelectedTab == Tab)
{
+ var color = style.BackgroundSelected;
+ if (!enabled)
+ color *= 0.6f;
if (Tabs._orientation == Orientation.Horizontal)
{
- Render2D.FillRectangle(tabRect, style.BackgroundSelected);
+ Render2D.FillRectangle(tabRect, color);
}
else
{
@@ -84,10 +81,10 @@ namespace FlaxEditor.GUI.Tabs
fillRect.Size.X -= lefEdgeWidth;
fillRect.Location.X += lefEdgeWidth;
Render2D.FillRectangle(fillRect, style.Background);
- Render2D.FillRectangle(leftEdgeRect, style.BackgroundSelected);
+ Render2D.FillRectangle(leftEdgeRect, color);
}
}
- else if (isMouseOverTab)
+ else if (IsMouseOver && enabled)
{
Render2D.FillRectangle(tabRect, style.BackgroundHighlighted);
}
@@ -131,20 +128,15 @@ namespace FlaxEditor.GUI.Tabs
{
base.PerformLayoutBeforeChildren();
- // Cache data
- var tabsSize = Tabs._tabsSize;
- var clientSize = GetClientArea();
- tabsSize = Float2.Min(tabsSize, clientSize.Size);
- var tabRect = new Rectangle(Float2.Zero, tabsSize);
- var tabStripOffset = Tabs._orientation == Orientation.Horizontal ? new Float2(tabsSize.X, 0) : new Float2(0, tabsSize.Y);
-
// Arrange tab header controls
+ var pos = Float2.Zero;
+ var sizeMask = Tabs._orientation == Orientation.Horizontal ? Float2.UnitX : Float2.UnitY;
for (int i = 0; i < Children.Count; i++)
{
if (Children[i] is TabHeader tabHeader)
{
- tabHeader.Bounds = tabRect;
- tabRect.Offset(tabStripOffset);
+ tabHeader.Location = pos;
+ pos += tabHeader.Size * sizeMask;
}
}
}
@@ -160,6 +152,11 @@ namespace FlaxEditor.GUI.Tabs
///
protected Float2 _tabsSize;
+ ///
+ /// Automatic tab size based on the fill axis.
+ ///
+ protected bool _autoTabsSizeAuto;
+
///
/// The orientation.
///
@@ -174,15 +171,31 @@ namespace FlaxEditor.GUI.Tabs
set
{
_tabsSize = value;
- for (int i = 0; i < TabsPanel.ChildrenCount; i++)
+ if (!_autoTabsSizeAuto)
{
- if (TabsPanel.Children[i] is TabHeader tabHeader)
- tabHeader.Size = value;
+ for (int i = 0; i < TabsPanel.ChildrenCount; i++)
+ {
+ if (TabsPanel.Children[i] is TabHeader tabHeader)
+ tabHeader.Size = value;
+ }
}
PerformLayout();
}
}
+ ///
+ /// Enables automatic tabs size to fill the space.
+ ///
+ public bool AutoTabsSize
+ {
+ get => _autoTabsSizeAuto;
+ set
+ {
+ _autoTabsSizeAuto = value;
+ PerformLayout();
+ }
+ }
+
///
/// Gets or sets the orientation.
///
@@ -339,14 +352,10 @@ namespace FlaxEditor.GUI.Tabs
// Update tabs headers
TabsPanel.DisposeChildren();
- int index = 0;
for (int i = 0; i < Children.Count; i++)
{
if (Children[i] is Tab tab)
- {
- var tabHeader = new TabHeader(this, index++, tab);
- TabsPanel.AddChild(tabHeader);
- }
+ TabsPanel.AddChild(tab.CreateHeader());
}
TabsPanel.IsLayoutLocked = wasLocked;
@@ -371,23 +380,46 @@ namespace FlaxEditor.GUI.Tabs
///
protected override void PerformLayoutBeforeChildren()
{
+ var tabsSize = _tabsSize;
+ if (_autoTabsSizeAuto)
+ {
+ // Horizontal is default for Toolbox so tabs go to the right
+ int tabsCount = 0;
+ for (int i = 0; i < TabsPanel.ChildrenCount; i++)
+ {
+ if (TabsPanel.Children[i] is TabHeader)
+ tabsCount++;
+ }
+ if (tabsCount == 0)
+ tabsCount = 1;
+ if (_orientation == Orientation.Horizontal)
+ tabsSize.X = Width / tabsCount;
+ else
+ tabsSize.Y = Height / tabsCount;
+ for (int i = 0; i < TabsPanel.ChildrenCount; i++)
+ {
+ if (TabsPanel.Children[i] is TabHeader tabHeader)
+ tabHeader.Size = tabsSize;
+ }
+ }
+
// Fit the tabs panel
- TabsPanel.Size = _orientation == Orientation.Horizontal ? new Float2(Width, _tabsSize.Y) : new Float2(_tabsSize.X, Height);
+ TabsPanel.Size = _orientation == Orientation.Horizontal
+ ? new Float2(Width, tabsSize.Y)
+ : new Float2(tabsSize.X, Height);
// Hide all pages except selected one
- var clientArea = _orientation == Orientation.Horizontal
- ? new Rectangle(0, _tabsSize.Y, Width, Height - _tabsSize.Y)
- : new Rectangle(_tabsSize.X, 0, Width - _tabsSize.X, Height);
for (int i = 0; i < Children.Count; i++)
{
if (Children[i] is Tab tab)
{
- // Check if is selected or not
if (i - 1 == _selectedIndex)
{
// Show and fit size
tab.Visible = true;
- tab.Bounds = clientArea;
+ tab.Bounds = _orientation == Orientation.Horizontal
+ ? new Rectangle(0, tabsSize.Y, Width, Height - tabsSize.Y)
+ : new Rectangle(tabsSize.X, 0, Width - tabsSize.X, Height);
}
else
{
diff --git a/Source/Editor/GUI/ToolStrip.cs b/Source/Editor/GUI/ToolStrip.cs
index 0dac241ed..360d63ffe 100644
--- a/Source/Editor/GUI/ToolStrip.cs
+++ b/Source/Editor/GUI/ToolStrip.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
+using FlaxEditor.Windows;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -181,5 +182,25 @@ namespace FlaxEditor.GUI
PerformLayout();
}
+
+ ///
+ public override bool OnKeyDown(KeyboardKeys key)
+ {
+ if (base.OnKeyDown(key))
+ return true;
+
+ // Fallback to the owning window for shortcuts
+ EditorWindow editorWindow = null;
+ ContainerControl c = Parent;
+ while (c != null && editorWindow == null)
+ {
+ editorWindow = c as EditorWindow;
+ c = c.Parent;
+ }
+ var editor = Editor.Instance;
+ if (editorWindow == null)
+ editorWindow = editor.Windows.EditWin; // Fallback to main editor window
+ return editorWindow.InputActions.Process(editor, this, key);
+ }
}
}
diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs
index ced70a281..7e3af05bf 100644
--- a/Source/Editor/GUI/Tree/TreeNode.cs
+++ b/Source/Editor/GUI/Tree/TreeNode.cs
@@ -762,6 +762,10 @@ namespace FlaxEditor.GUI.Tree
// Add/Remove
tree.AddOrRemoveSelection(this);
}
+ else if (button == MouseButton.Right && tree.Selection.Contains(this))
+ {
+ // Do nothing
+ }
else
{
// Select
diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs
index ecafbf5d4..29d6b2c00 100644
--- a/Source/Editor/Modules/ContentDatabaseModule.cs
+++ b/Source/Editor/Modules/ContentDatabaseModule.cs
@@ -902,6 +902,23 @@ namespace FlaxEditor.Modules
}
if (sortChildren)
node.SortChildren();
+
+ // Ignore some special folders
+ if (node is MainContentTreeNode mainNode && mainNode.Folder.ShortName == "Source")
+ {
+ var mainNodeChild = mainNode.Folder.Find(StringUtils.CombinePaths(mainNode.Path, "obj")) as ContentFolder;
+ if (mainNodeChild != null)
+ {
+ mainNodeChild.Visible = false;
+ mainNodeChild.Node.Visible = false;
+ }
+ mainNodeChild = mainNode.Folder.Find(StringUtils.CombinePaths(mainNode.Path, "Properties")) as ContentFolder;
+ if (mainNodeChild != null)
+ {
+ mainNodeChild.Visible = false;
+ mainNodeChild.Node.Visible = false;
+ }
+ }
}
private void LoadScripts(ContentTreeNode parent, string[] files)
diff --git a/Source/Editor/Modules/ContentFindingModule.cs b/Source/Editor/Modules/ContentFindingModule.cs
index ccf1cb3e4..9e8002c8a 100644
--- a/Source/Editor/Modules/ContentFindingModule.cs
+++ b/Source/Editor/Modules/ContentFindingModule.cs
@@ -166,6 +166,8 @@ namespace FlaxEditor.Modules
public void ShowFinder(Control control)
{
var finder = _finder ?? (_finder = new ContentFinder());
+ if (control == null)
+ control = Editor.Instance.Windows.MainWindow.GUI;
var position = (control.Size - new Float2(finder.Width, 300.0f)) * 0.5f;
finder.Show(control, position);
}
diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs
index 0a51038ac..257262585 100644
--- a/Source/Editor/Modules/SceneModule.cs
+++ b/Source/Editor/Modules/SceneModule.cs
@@ -591,7 +591,7 @@ namespace FlaxEditor.Modules
private void OnActorNameChanged(Actor actor)
{
ActorNode node = GetActorNode(actor);
- node?.TreeNode.OnNameChanged();
+ node?.TreeNode.UpdateText();
}
private void OnActorActiveChanged(Actor actor)
diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs
index eebee0cd9..1b35cdc35 100644
--- a/Source/Editor/SceneGraph/Actors/SplineNode.cs
+++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs
@@ -21,7 +21,7 @@ namespace FlaxEditor.SceneGraph.Actors
[HideInEditor]
public sealed class SplineNode : ActorNode
{
- private sealed class SplinePointNode : ActorChildNode
+ internal sealed class SplinePointNode : ActorChildNode
{
public unsafe SplinePointNode(SplineNode node, Guid id, int index)
: base(node, id, index)
@@ -167,10 +167,11 @@ namespace FlaxEditor.SceneGraph.Actors
public override bool RayCastSelf(ref RayCastData ray, out Real distance, out Vector3 normal)
{
+ normal = -ray.Ray.Direction;
var actor = (Spline)_node.Actor;
var pos = actor.GetSplinePoint(Index);
- normal = -ray.Ray.Direction;
- return new BoundingSphere(pos, 7.0f).Intersects(ref ray.Ray, out distance);
+ var nodeSize = NodeSizeByDistance(Transform.Translation, PointNodeSize);
+ return new BoundingSphere(pos, nodeSize).Intersects(ref ray.Ray, out distance);
}
public override void OnDebugDraw(ViewportDebugDrawData data)
@@ -179,23 +180,26 @@ namespace FlaxEditor.SceneGraph.Actors
var pos = actor.GetSplinePoint(Index);
var tangentIn = actor.GetSplineTangent(Index, true).Translation;
var tangentOut = actor.GetSplineTangent(Index, false).Translation;
+ var pointSize = NodeSizeByDistance(pos, PointNodeSize);
+ var tangentInSize = NodeSizeByDistance(tangentIn, TangentNodeSize);
+ var tangentOutSize = NodeSizeByDistance(tangentOut, TangentNodeSize);
// Draw spline path
ParentNode.OnDebugDraw(data);
// Draw selected point highlight
- DebugDraw.DrawSphere(new BoundingSphere(pos, 5.0f), Color.Yellow, 0, false);
+ DebugDraw.DrawSphere(new BoundingSphere(pos, pointSize), Color.Yellow, 0, false);
// Draw tangent points
if (tangentIn != pos)
{
DebugDraw.DrawLine(pos, tangentIn, Color.Blue.AlphaMultiplied(0.6f), 0, false);
- DebugDraw.DrawWireSphere(new BoundingSphere(tangentIn, 4.0f), Color.Blue, 0, false);
+ DebugDraw.DrawWireSphere(new BoundingSphere(tangentIn, tangentInSize), Color.Blue, 0, false);
}
if (tangentOut != pos)
{
DebugDraw.DrawLine(pos, tangentOut, Color.Red.AlphaMultiplied(0.6f), 0, false);
- DebugDraw.DrawWireSphere(new BoundingSphere(tangentOut, 4.0f), Color.Red, 0, false);
+ DebugDraw.DrawWireSphere(new BoundingSphere(tangentOut, tangentOutSize), Color.Red, 0, false);
}
}
@@ -219,7 +223,7 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
- private sealed class SplinePointTangentNode : ActorChildNode
+ internal sealed class SplinePointTangentNode : ActorChildNode
{
private SplineNode _node;
private int _index;
@@ -249,10 +253,11 @@ namespace FlaxEditor.SceneGraph.Actors
public override bool RayCastSelf(ref RayCastData ray, out Real distance, out Vector3 normal)
{
+ normal = -ray.Ray.Direction;
var actor = (Spline)_node.Actor;
var pos = actor.GetSplineTangent(_index, _isIn).Translation;
- normal = -ray.Ray.Direction;
- return new BoundingSphere(pos, 7.0f).Intersects(ref ray.Ray, out distance);
+ var tangentSize = NodeSizeByDistance(Transform.Translation, TangentNodeSize);
+ return new BoundingSphere(pos, tangentSize).Intersects(ref ray.Ray, out distance);
}
public override void OnDebugDraw(ViewportDebugDrawData data)
@@ -263,7 +268,8 @@ namespace FlaxEditor.SceneGraph.Actors
// Draw selected tangent highlight
var actor = (Spline)_node.Actor;
var pos = actor.GetSplineTangent(_index, _isIn).Translation;
- DebugDraw.DrawSphere(new BoundingSphere(pos, 5.0f), Color.YellowGreen, 0, false);
+ var tangentSize = NodeSizeByDistance(Transform.Translation, TangentNodeSize);
+ DebugDraw.DrawSphere(new BoundingSphere(pos, tangentSize), Color.YellowGreen, 0, false);
}
public override void OnContextMenu(ContextMenu contextMenu)
@@ -279,6 +285,9 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
+ private const Real PointNodeSize = 1.5f;
+ private const Real TangentNodeSize = 1.0f;
+
///
public SplineNode(Actor actor)
: base(actor)
@@ -398,6 +407,13 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
+ internal static Real NodeSizeByDistance(Vector3 nodePosition, Real nodeSize)
+ {
+ var cameraTransform = Editor.Instance.Windows.EditWin.Viewport.ViewportCamera.Viewport.ViewTransform;
+ var distance = Vector3.Distance(cameraTransform.Translation, nodePosition) / 100;
+ return distance * nodeSize;
+ }
+
///
public override bool RayCastSelf(ref RayCastData ray, out Real distance, out Vector3 normal)
{
diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
index 83ea666d9..c4edda65e 100644
--- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
+++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
@@ -97,11 +97,6 @@ namespace FlaxEditor.SceneGraph.GUI
}
}
- internal void OnNameChanged()
- {
- UpdateText();
- }
-
///
/// Updates the tree node text.
///
diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs
index dd861d4af..1389a6904 100644
--- a/Source/Editor/Surface/Archetypes/Particles.cs
+++ b/Source/Editor/Surface/Archetypes/Particles.cs
@@ -553,6 +553,18 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 0),
}
},
+ new NodeArchetype
+ {
+ TypeID = 112,
+ Title = "Particle Scale",
+ Description = "Particle scale.",
+ Flags = NodeFlags.ParticleEmitterGraph,
+ Size = new Float2(200, 30),
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float3), 0),
+ }
+ },
// Simulation data access nodes
new NodeArchetype
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
index 1b1cfb4ff..5a532e362 100644
--- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
+++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
@@ -430,7 +430,7 @@ namespace FlaxEditor.Windows.Assets
{
if (actorNode.Actor)
{
- actorNode.TreeNode.OnNameChanged();
+ actorNode.TreeNode.UpdateText();
actorNode.TreeNode.OnOrderInParentChanged();
}
diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs
index ad690c2d0..b1def7ae8 100644
--- a/Source/Editor/Windows/ContentWindow.Navigation.cs
+++ b/Source/Editor/Windows/ContentWindow.Navigation.cs
@@ -202,6 +202,13 @@ namespace FlaxEditor.Windows
button.PerformLayout();
x += button.Width + NavigationBar.DefaultButtonsMargin;
_navigationBar.AddChild(button);
+ if (i > 0)
+ {
+ var separator = new ContentNavigationSeparator(button, x, ToolStrip.DefaultMarginV, h);
+ separator.PerformLayout();
+ x += separator.Width + NavigationBar.DefaultButtonsMargin;
+ _navigationBar.AddChild(separator);
+ }
}
nodes.Clear();
@@ -218,21 +225,13 @@ namespace FlaxEditor.Windows
///
/// Gets the current view folder.
///
- public ContentFolder CurrentViewFolder
- {
- get
- {
- var node = SelectedNode;
- return node?.Folder;
- }
- }
+ public ContentFolder CurrentViewFolder => SelectedNode?.Folder;
///
/// Shows the root folder.
///
public void ShowRoot()
{
- // Show root folder
_tree.Select(_root);
}
}
diff --git a/Source/Editor/Windows/ContentWindow.Search.cs b/Source/Editor/Windows/ContentWindow.Search.cs
index a6fd2c81e..a1072d158 100644
--- a/Source/Editor/Windows/ContentWindow.Search.cs
+++ b/Source/Editor/Windows/ContentWindow.Search.cs
@@ -191,6 +191,7 @@ namespace FlaxEditor.Windows
}
// Search by filter only
+ bool showAllFiles = _showAllFiles;
if (string.IsNullOrWhiteSpace(query))
{
if (SelectedNode == _root)
@@ -199,12 +200,12 @@ namespace FlaxEditor.Windows
for (int i = 0; i < _root.ChildrenCount; i++)
{
if (_root.GetChild(i) is ContentTreeNode node)
- UpdateItemsSearchFilter(node.Folder, items, filters);
+ UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles);
}
}
else
{
- UpdateItemsSearchFilter(CurrentViewFolder, items, filters);
+ UpdateItemsSearchFilter(CurrentViewFolder, items, filters, showAllFiles);
}
}
// Search by asset ID
@@ -221,12 +222,12 @@ namespace FlaxEditor.Windows
for (int i = 0; i < _root.ChildrenCount; i++)
{
if (_root.GetChild(i) is ContentTreeNode node)
- UpdateItemsSearchFilter(node.Folder, items, filters, query);
+ UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles, query);
}
}
else
{
- UpdateItemsSearchFilter(CurrentViewFolder, items, filters, query);
+ UpdateItemsSearchFilter(CurrentViewFolder, items, filters, showAllFiles, query);
}
}
@@ -234,42 +235,34 @@ namespace FlaxEditor.Windows
_view.ShowItems(items, _sortType);
}
- private void UpdateItemsSearchFilter(ContentFolder folder, List items, bool[] filters)
+ private void UpdateItemsSearchFilter(ContentFolder folder, List items, bool[] filters, bool showAllFiles)
{
for (int i = 0; i < folder.Children.Count; i++)
{
var child = folder.Children[i];
-
if (child is ContentFolder childFolder)
{
- UpdateItemsSearchFilter(childFolder, items, filters);
+ UpdateItemsSearchFilter(childFolder, items, filters, showAllFiles);
}
- else
+ else if (filters[(int)child.SearchFilter] && (showAllFiles || !(child is FileItem)))
{
- if (filters[(int)child.SearchFilter])
- {
- items.Add(child);
- }
+ items.Add(child);
}
}
}
- private void UpdateItemsSearchFilter(ContentFolder folder, List items, bool[] filters, string filterText)
+ private void UpdateItemsSearchFilter(ContentFolder folder, List items, bool[] filters, bool showAllFiles, string filterText)
{
for (int i = 0; i < folder.Children.Count; i++)
{
var child = folder.Children[i];
-
if (child is ContentFolder childFolder)
{
- UpdateItemsSearchFilter(childFolder, items, filters, filterText);
+ UpdateItemsSearchFilter(childFolder, items, filters, showAllFiles, filterText);
}
- else if (filters[(int)child.SearchFilter])
+ else if (filters[(int)child.SearchFilter] && (showAllFiles || !(child is FileItem)) && QueryFilterHelper.Match(filterText, child.ShortName))
{
- if (filters[(int)child.SearchFilter] && QueryFilterHelper.Match(filterText, child.ShortName))
- {
- items.Add(child);
- }
+ items.Add(child);
}
}
}
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index b1f04c8f5..0f3c12286 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Xml;
using FlaxEditor.Content;
using FlaxEditor.Content.GUI;
@@ -45,6 +46,7 @@ namespace FlaxEditor.Windows
private TextBox _itemsSearchBox;
private ViewDropdown _viewDropdown;
private SortType _sortType;
+ private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true;
private RootContentTreeNode _root;
@@ -64,6 +66,59 @@ namespace FlaxEditor.Windows
///
public ContentView View => _view;
+ internal bool ShowEngineFiles
+ {
+ get => _showEngineFiles;
+ set
+ {
+ if (_showEngineFiles != value)
+ {
+ _showEngineFiles = value;
+ if (Editor.ContentDatabase.Engine != null)
+ {
+ Editor.ContentDatabase.Engine.Visible = value;
+ Editor.ContentDatabase.Engine.Folder.Visible = value;
+ RefreshView();
+ _tree.PerformLayout();
+ }
+ }
+ }
+ }
+
+ internal bool ShowPluginsFiles
+ {
+ get => _showPluginsFiles;
+ set
+ {
+ if (_showPluginsFiles != value)
+ {
+ _showPluginsFiles = value;
+ foreach (var project in Editor.ContentDatabase.Projects)
+ {
+ if (project == Editor.ContentDatabase.Game || project == Editor.ContentDatabase.Engine)
+ continue;
+ project.Visible = value;
+ project.Folder.Visible = value;
+ RefreshView();
+ _tree.PerformLayout();
+ }
+ }
+ }
+ }
+
+ internal bool ShowAllFiles
+ {
+ get => _showAllFiles;
+ set
+ {
+ if (_showAllFiles != value)
+ {
+ _showAllFiles = value;
+ RefreshView();
+ }
+ }
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -107,11 +162,12 @@ namespace FlaxEditor.Windows
_navigateBackwardButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Left64, NavigateBackward).LinkTooltip("Navigate backward");
_navigateForwardButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Right64, NavigateForward).LinkTooltip("Navigate forward");
_navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up");
+ _toolStrip.AddSeparator();
// Navigation bar
_navigationBar = new NavigationBar
{
- Parent = this,
+ Parent = _toolStrip,
};
// Split panel
@@ -216,10 +272,6 @@ namespace FlaxEditor.Windows
{
var menu = new ContextMenu();
- var showFileExtensionsButton = menu.AddButton("Show file extensions", () => View.ShowFileExtensions = !View.ShowFileExtensions);
- showFileExtensionsButton.Checked = View.ShowFileExtensions;
- showFileExtensionsButton.AutoCheck = true;
-
var viewScale = menu.AddButton("View Scale");
viewScale.CloseMenuOnClick = false;
var scaleValue = new FloatValueBox(1, 75, 2, 50.0f, 0.3f, 3.0f, 0.01f)
@@ -243,6 +295,33 @@ namespace FlaxEditor.Windows
}
};
+ var show = menu.AddChildMenu("Show");
+ {
+ var b = show.ContextMenu.AddButton("File extensions", () => View.ShowFileExtensions = !View.ShowFileExtensions);
+ b.TooltipText = "Shows all files with extensions";
+ b.Checked = View.ShowFileExtensions;
+ b.CloseMenuOnClick = false;
+ b.AutoCheck = true;
+
+ b = show.ContextMenu.AddButton("Engine files", () => ShowEngineFiles = !ShowEngineFiles);
+ b.TooltipText = "Shows in-built engine content";
+ b.Checked = ShowEngineFiles;
+ b.CloseMenuOnClick = false;
+ b.AutoCheck = true;
+
+ b = show.ContextMenu.AddButton("Plugins files", () => ShowPluginsFiles = !ShowPluginsFiles);
+ b.TooltipText = "Shows plugin projects content";
+ b.Checked = ShowPluginsFiles;
+ b.CloseMenuOnClick = false;
+ b.AutoCheck = true;
+
+ b = show.ContextMenu.AddButton("All files", () => ShowAllFiles = !ShowAllFiles);
+ b.TooltipText = "Shows all files including other than assets and source code";
+ b.Checked = ShowAllFiles;
+ b.CloseMenuOnClick = false;
+ b.AutoCheck = true;
+ }
+
var filters = menu.AddChildMenu("Filters");
for (int i = 0; i < _viewDropdown.Items.Count; i++)
{
@@ -351,7 +430,7 @@ namespace FlaxEditor.Windows
{
if (!item.CanRename)
return;
-
+
// Show element in the view
Select(item, true);
@@ -428,10 +507,7 @@ namespace FlaxEditor.Windows
if (!Editor.ContentEditing.IsValidAssetName(item, newShortName, out string hint))
{
// Invalid name
- MessageBox.Show("Given asset name is invalid. " + hint,
- "Invalid name",
- MessageBoxButtons.OK,
- MessageBoxIcon.Error);
+ MessageBox.Show("Given asset name is invalid. " + hint, "Invalid name", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
@@ -877,7 +953,7 @@ namespace FlaxEditor.Windows
if (target == _root)
{
// Special case for root folder
- List items = new List(8);
+ var items = new List(8);
for (int i = 0; i < _root.ChildrenCount; i++)
{
if (_root.GetChild(i) is ContentTreeNode node)
@@ -890,7 +966,10 @@ namespace FlaxEditor.Windows
else
{
// Show folder contents
- _view.ShowItems(target.Folder.Children, _sortType, false, true);
+ var items = target.Folder.Children;
+ if (!_showAllFiles)
+ items = items.Where(x => !(x is FileItem)).ToList();
+ _view.ShowItems(items, _sortType, false, true);
}
}
@@ -913,12 +992,6 @@ namespace FlaxEditor.Windows
_navigateUpButton.Enabled = folder != null && _tree.SelectedNode != _root;
}
- private void RemoveFolder2Root(ContentTreeNode node)
- {
- // Remove from the root
- _root.RemoveChild(node);
- }
-
///
public override void OnInit()
{
@@ -931,13 +1004,18 @@ namespace FlaxEditor.Windows
// Add game project on top, plugins in the middle and engine at bottom
_root.AddChild(Editor.ContentDatabase.Game);
+ Editor.ContentDatabase.Projects.Sort();
foreach (var project in Editor.ContentDatabase.Projects)
{
project.SortChildrenRecursive();
if (project == Editor.ContentDatabase.Game || project == Editor.ContentDatabase.Engine)
continue;
+ project.Visible = _showPluginsFiles;
+ project.Folder.Visible = _showPluginsFiles;
_root.AddChild(project);
}
+ Editor.ContentDatabase.Engine.Visible = _showEngineFiles;
+ Editor.ContentDatabase.Engine.Folder.Visible = _showEngineFiles;
_root.AddChild(Editor.ContentDatabase.Engine);
Editor.ContentDatabase.Game?.Expand(true);
@@ -1009,7 +1087,6 @@ namespace FlaxEditor.Windows
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
- // Check if it's a right mouse button
if (button == MouseButton.Right)
{
// Find control that is under the mouse
@@ -1065,6 +1142,9 @@ namespace FlaxEditor.Windows
LayoutSerializeSplitter(writer, "Split", _split);
writer.WriteAttributeString("Scale", _view.ViewScale.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("ShowFileExtensions", _view.ShowFileExtensions.ToString());
+ writer.WriteAttributeString("ShowEngineFiles", ShowEngineFiles.ToString());
+ writer.WriteAttributeString("ShowPluginsFiles", ShowPluginsFiles.ToString());
+ writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString());
writer.WriteAttributeString("ViewType", _view.ViewType.ToString());
}
@@ -1076,6 +1156,12 @@ namespace FlaxEditor.Windows
_view.ViewScale = value1;
if (bool.TryParse(node.GetAttribute("ShowFileExtensions"), out bool value2))
_view.ShowFileExtensions = value2;
+ if (bool.TryParse(node.GetAttribute("ShowEngineFiles"), out value2))
+ ShowEngineFiles = value2;
+ if (bool.TryParse(node.GetAttribute("ShowPluginsFiles"), out value2))
+ ShowPluginsFiles = value2;
+ if (bool.TryParse(node.GetAttribute("ShowAllFiles"), out value2))
+ ShowAllFiles = value2;
if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType))
_view.ViewType = viewType;
}
diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs
index 694551345..c5ec02b31 100644
--- a/Source/Editor/Windows/PluginsWindow.cs
+++ b/Source/Editor/Windows/PluginsWindow.cs
@@ -2,11 +2,18 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
using FlaxEditor.GUI;
+using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
+using FlaxEngine.Json;
namespace FlaxEditor.Windows
{
@@ -17,6 +24,8 @@ namespace FlaxEditor.Windows
public sealed class PluginsWindow : EditorWindow
{
private Tabs _tabs;
+ private Button _addPluginProjectButton;
+ private Button _cloneProjectButton;
private readonly List _categories = new List();
private readonly Dictionary _entries = new Dictionary();
@@ -174,19 +183,576 @@ namespace FlaxEditor.Windows
{
Title = "Plugins";
+ var vp = new Panel
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Parent = this,
+ };
+ _addPluginProjectButton = new Button
+ {
+ Text = "Create Plugin Project",
+ TooltipText = "Add new plugin project.",
+ AnchorPreset = AnchorPresets.TopLeft,
+ LocalLocation = new Float2(70, 18),
+ Size = new Float2(150, 25),
+ Parent = vp,
+ };
+ _addPluginProjectButton.Clicked += OnAddButtonClicked;
+
+ _cloneProjectButton = new Button
+ {
+ Text = "Clone Plugin Project",
+ TooltipText = "Git Clone a plugin project.",
+ AnchorPreset = AnchorPresets.TopLeft,
+ LocalLocation = new Float2(70 + _addPluginProjectButton.Size.X + 8, 18),
+ Size = new Float2(150, 25),
+ Parent = vp,
+ };
+ _cloneProjectButton.Clicked += OnCloneProjectButtonClicked;
+
_tabs = new Tabs
{
Orientation = Orientation.Vertical,
AnchorPreset = AnchorPresets.StretchAll,
- Offsets = Margin.Zero,
+ Offsets = new Margin(0, 0, _addPluginProjectButton.Bottom + 8, 0),
TabsSize = new Float2(120, 32),
- Parent = this
+ Parent = vp
};
OnPluginsChanged();
PluginManager.PluginsChanged += OnPluginsChanged;
}
+ private void OnCloneProjectButtonClicked()
+ {
+ var popup = new ContextMenuBase
+ {
+ Size = new Float2(300, 125),
+ ClipChildren = false,
+ CullChildren = false,
+ };
+ popup.Show(_cloneProjectButton, new Float2(_cloneProjectButton.Width, 0));
+
+ var nameLabel = new Label
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ AutoWidth = true,
+ Text = "Name",
+ HorizontalAlignment = TextAlignment.Near,
+ };
+ nameLabel.LocalY += 10;
+
+ var nameTextBox = new TextBox
+ {
+ Parent = popup,
+ WatermarkText = "Plugin Name",
+ TooltipText = "If left blank, this will take the git name.",
+ AnchorPreset = AnchorPresets.TopLeft,
+ IsMultiline = false,
+ };
+ nameTextBox.LocalX += (300 - (10)) * 0.5f;
+ nameTextBox.LocalY += 10;
+ nameLabel.LocalX += (300 - (nameLabel.Width + nameTextBox.Width)) * 0.5f + 10;
+
+ var defaultTextBoxBorderColor = nameTextBox.BorderColor;
+ var defaultTextBoxBorderSelectedColor = nameTextBox.BorderSelectedColor;
+ nameTextBox.TextChanged += () =>
+ {
+ if (string.IsNullOrEmpty(nameTextBox.Text))
+ {
+ nameTextBox.BorderColor = defaultTextBoxBorderColor;
+ nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor;
+ return;
+ }
+
+ var pluginPath = Path.Combine(Globals.ProjectFolder, "Plugins", nameTextBox.Text);
+ if (Directory.Exists(pluginPath))
+ {
+ nameTextBox.BorderColor = Color.Red;
+ nameTextBox.BorderSelectedColor = Color.Red;
+ }
+ else
+ {
+ nameTextBox.BorderColor = defaultTextBoxBorderColor;
+ nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor;
+ }
+ };
+
+ var gitPathLabel = new Label
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ AutoWidth = true,
+ Text = "Git Path",
+ HorizontalAlignment = TextAlignment.Near,
+ };
+ gitPathLabel.LocalX += (300 - gitPathLabel.Width) * 0.5f;
+ gitPathLabel.LocalY += 35;
+
+ var gitPathTextBox = new TextBox
+ {
+ Parent = popup,
+ WatermarkText = "https://github.com/FlaxEngine/ExamplePlugin.git",
+ AnchorPreset = AnchorPresets.TopLeft,
+ Size = new Float2(280, TextBox.DefaultHeight),
+ IsMultiline = false,
+ };
+ gitPathTextBox.LocalY += 60;
+ gitPathTextBox.LocalX += 10;
+
+ var submitButton = new Button
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Clone",
+ Width = 70,
+ };
+ submitButton.LocalX += 300 * 0.5f - submitButton.Width - 10;
+ submitButton.LocalY += 90;
+
+ submitButton.Clicked += () =>
+ {
+ if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Plugins", nameTextBox.Text)) && !string.IsNullOrEmpty(nameTextBox.Text))
+ {
+ Editor.LogWarning("Cannot create plugin due to name conflict.");
+ return;
+ }
+ OnCloneButtonClicked(nameTextBox.Text, gitPathTextBox.Text);
+ nameTextBox.Clear();
+ gitPathTextBox.Clear();
+ popup.Hide();
+ };
+
+ var cancelButton = new Button
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Cancel",
+ Width = 70,
+ };
+ cancelButton.LocalX += 300 * 0.5f + 10;
+ cancelButton.LocalY += 90;
+
+ cancelButton.Clicked += () =>
+ {
+ nameTextBox.Clear();
+ gitPathTextBox.Clear();
+ popup.Hide();
+ };
+ }
+
+ private async void OnCloneButtonClicked(string pluginName, string gitPath)
+ {
+ if (string.IsNullOrEmpty(gitPath))
+ {
+ Editor.LogError("Failed to create plugin project due to no GIT path.");
+ return;
+ }
+ if (string.IsNullOrEmpty(pluginName))
+ {
+ var split = gitPath.Split('/');
+ if (string.IsNullOrEmpty(split[^1]))
+ {
+ var name = split[^2].Replace(".git", "");
+ pluginName = name;
+ }
+ else
+ {
+ var name = split[^1].Replace(".git", "");
+ pluginName = name;
+ }
+ }
+
+ var clonePath = Path.Combine(Globals.ProjectFolder, "Plugins", pluginName);
+ if (!Directory.Exists(clonePath))
+ Directory.CreateDirectory(clonePath);
+ else
+ {
+ Editor.LogError("Plugin Name is already used. Pick a different Name.");
+ return;
+ }
+ try
+ {
+ // Start git clone
+ var settings = new CreateProcessSettings
+ {
+ FileName = "git",
+ Arguments = $"clone {gitPath} \"{clonePath}\"",
+ ShellExecute = false,
+ LogOutput = true,
+ };
+ Platform.CreateProcess(ref settings);
+ }
+ catch (Exception e)
+ {
+ Editor.LogError($"Failed Git process. {e}");
+ return;
+ }
+
+ Editor.Log("Plugin project has been cloned.");
+
+ // Find project config file. Could be different then what the user named the folder.
+ var files = Directory.GetFiles(clonePath);
+ string pluginProjectName = "";
+ foreach (var file in files)
+ {
+ if (file.Contains(".flaxproj", StringComparison.OrdinalIgnoreCase))
+ {
+ pluginProjectName = Path.GetFileNameWithoutExtension(file);
+ Debug.Log(pluginProjectName);
+ }
+ }
+
+ if (string.IsNullOrEmpty(pluginProjectName))
+ Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually.");
+ else
+ {
+ await AddReferenceToProject(pluginName, pluginProjectName);
+ MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
+ }
+ }
+
+ private void OnAddButtonClicked()
+ {
+ var popup = new ContextMenuBase
+ {
+ Size = new Float2(230, 125),
+ ClipChildren = false,
+ CullChildren = false,
+ };
+ popup.Show(_addPluginProjectButton, new Float2(_addPluginProjectButton.Width, 0));
+
+ var nameLabel = new Label
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Name",
+ HorizontalAlignment = TextAlignment.Near,
+ };
+ nameLabel.LocalX += 10;
+ nameLabel.LocalY += 10;
+
+ var nameTextBox = new TextBox
+ {
+ Parent = popup,
+ WatermarkText = "Plugin Name",
+ AnchorPreset = AnchorPresets.TopLeft,
+ IsMultiline = false,
+ };
+ nameTextBox.LocalX += 100;
+ nameTextBox.LocalY += 10;
+ var defaultTextBoxBorderColor = nameTextBox.BorderColor;
+ var defaultTextBoxBorderSelectedColor = nameTextBox.BorderSelectedColor;
+ nameTextBox.TextChanged += () =>
+ {
+ if (string.IsNullOrEmpty(nameTextBox.Text))
+ {
+ nameTextBox.BorderColor = defaultTextBoxBorderColor;
+ nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor;
+ return;
+ }
+
+ var pluginPath = Path.Combine(Globals.ProjectFolder, "Plugins", nameTextBox.Text);
+ if (Directory.Exists(pluginPath))
+ {
+ nameTextBox.BorderColor = Color.Red;
+ nameTextBox.BorderSelectedColor = Color.Red;
+ }
+ else
+ {
+ nameTextBox.BorderColor = defaultTextBoxBorderColor;
+ nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor;
+ }
+ };
+
+ var versionLabel = new Label
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Version",
+ HorizontalAlignment = TextAlignment.Near,
+ };
+ versionLabel.LocalX += 10;
+ versionLabel.LocalY += 35;
+
+ var versionTextBox = new TextBox
+ {
+ Parent = popup,
+ WatermarkText = "1.0.0",
+ AnchorPreset = AnchorPresets.TopLeft,
+ IsMultiline = false,
+ };
+ versionTextBox.LocalY += 35;
+ versionTextBox.LocalX += 100;
+
+ var companyLabel = new Label
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Company",
+ HorizontalAlignment = TextAlignment.Near,
+ };
+ companyLabel.LocalX += 10;
+ companyLabel.LocalY += 60;
+
+ var companyTextBox = new TextBox
+ {
+ Parent = popup,
+ WatermarkText = "Company Name",
+ AnchorPreset = AnchorPresets.TopLeft,
+ IsMultiline = false,
+ };
+ companyTextBox.LocalY += 60;
+ companyTextBox.LocalX += 100;
+
+ var submitButton = new Button
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Create",
+ Width = 70,
+ };
+ submitButton.LocalX += 40;
+ submitButton.LocalY += 90;
+
+ submitButton.Clicked += () =>
+ {
+ if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Plugins", nameTextBox.Text)))
+ {
+ Editor.LogWarning("Cannot create plugin due to name conflict.");
+ return;
+ }
+ OnCreateButtonClicked(nameTextBox.Text, versionTextBox.Text, companyTextBox.Text);
+ nameTextBox.Clear();
+ versionTextBox.Clear();
+ companyTextBox.Clear();
+ popup.Hide();
+ };
+
+ var cancelButton = new Button
+ {
+ Parent = popup,
+ AnchorPreset = AnchorPresets.TopLeft,
+ Text = "Cancel",
+ Width = 70,
+ };
+ cancelButton.LocalX += 120;
+ cancelButton.LocalY += 90;
+
+ cancelButton.Clicked += () =>
+ {
+ nameTextBox.Clear();
+ versionTextBox.Clear();
+ companyTextBox.Clear();
+ popup.Hide();
+ };
+ }
+
+ private async void OnCreateButtonClicked(string pluginName, string pluginVersion, string companyName)
+ {
+ if (string.IsNullOrEmpty(pluginName))
+ {
+ Editor.LogError("Failed to create plugin project due to no plugin name.");
+ return;
+ }
+
+ var templateUrl = "https://github.com/FlaxEngine/ExamplePlugin/archive/refs/heads/master.zip";
+ var localTemplateFolderLocation = Path.Combine(Editor.LocalCachePath, "TemplatePluginCache");
+ if (!Directory.Exists(localTemplateFolderLocation))
+ Directory.CreateDirectory(localTemplateFolderLocation);
+ var localTemplatePath = Path.Combine(localTemplateFolderLocation, "TemplatePlugin.zip");
+
+ try
+ {
+ // Download example plugin
+ using (var client = new HttpClient())
+ {
+ byte[] zipBytes = await client.GetByteArrayAsync(templateUrl);
+ await File.WriteAllBytesAsync(!File.Exists(localTemplatePath) ? Path.Combine(localTemplatePath) : Path.Combine(Editor.LocalCachePath, "TemplatePluginCache", "TemplatePlugin1.zip"), zipBytes);
+
+ Editor.Log("Template for plugin project has downloaded");
+ }
+ }
+ catch (Exception e)
+ {
+ Editor.LogError($"Failed to download template project. Trying to use local file. {e}");
+ if (!File.Exists(localTemplatePath))
+ {
+ Editor.LogError("Failed to use local file. Does not exist.");
+ return;
+ }
+ }
+
+ // Check if any changes in new downloaded file
+ if (File.Exists(Path.Combine(Editor.LocalCachePath, "TemplatePluginCache", "TemplatePlugin1.zip")))
+ {
+ var localTemplatePath2 = Path.Combine(Editor.LocalCachePath, "TemplatePluginCache", "TemplatePlugin1.zip");
+ bool areDifferent = false;
+ using (var zip1 = ZipFile.OpenRead(localTemplatePath))
+ {
+ using (var zip2 = ZipFile.OpenRead(localTemplatePath2))
+ {
+ if (zip1.Entries.Count != zip2.Entries.Count)
+ {
+ areDifferent = true;
+ }
+
+ foreach (ZipArchiveEntry entry1 in zip1.Entries)
+ {
+ ZipArchiveEntry entry2 = zip2.GetEntry(entry1.FullName);
+ if (entry2 == null)
+ {
+ areDifferent = true;
+ break;
+ }
+ if (entry1.Length != entry2.Length || entry1.CompressedLength != entry2.CompressedLength || entry1.Crc32 != entry2.Crc32)
+ {
+ areDifferent = true;
+ break;
+ }
+ }
+ }
+ }
+ if (areDifferent)
+ {
+ File.Delete(localTemplatePath);
+ File.Move(localTemplatePath2, localTemplatePath);
+ }
+ else
+ {
+ File.Delete(localTemplatePath2);
+ }
+ }
+
+ var extractPath = Path.Combine(Globals.ProjectFolder, "Plugins");
+ if (!Directory.Exists(extractPath))
+ Directory.CreateDirectory(extractPath);
+
+ try
+ {
+ await Task.Run(() => ZipFile.ExtractToDirectory(localTemplatePath, extractPath));
+ Editor.Log("Template for plugin project successfully moved to project.");
+ }
+ catch (IOException e)
+ {
+ Editor.LogError($"Failed to add plugin to project. {e}");
+ }
+
+ // Format plugin name into a valid name for code (C#/C++ typename)
+ var pluginCodeName = Content.ScriptItem.FilterScriptName(pluginName);
+ if (string.IsNullOrEmpty(pluginCodeName))
+ pluginCodeName = "MyPlugin";
+ Editor.Log($"Using plugin code type name: {pluginCodeName}");
+
+ var oldPluginPath = Path.Combine(extractPath, "ExamplePlugin-master");
+ var newPluginPath = Path.Combine(extractPath, pluginName);
+ Directory.Move(oldPluginPath, newPluginPath);
+
+ var oldFlaxProjFile = Path.Combine(newPluginPath, "ExamplePlugin.flaxproj");
+ var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginName}.flaxproj");
+ File.Move(oldFlaxProjFile, newFlaxProjFile);
+
+ var readme = Path.Combine(newPluginPath, "README.md");
+ if (File.Exists(readme))
+ File.Delete(readme);
+ var license = Path.Combine(newPluginPath, "LICENSE");
+ if (File.Exists(license))
+ File.Delete(license);
+
+ // Flax plugin project file
+ var flaxPluginProjContents = JsonSerializer.Deserialize(await File.ReadAllTextAsync(newFlaxProjFile));
+ flaxPluginProjContents.Name = pluginName;
+ if (!string.IsNullOrEmpty(pluginVersion))
+ flaxPluginProjContents.Version = new Version(pluginVersion);
+ if (!string.IsNullOrEmpty(companyName))
+ flaxPluginProjContents.Company = companyName;
+ flaxPluginProjContents.GameTarget = $"{pluginCodeName}Target";
+ flaxPluginProjContents.EditorTarget = $"{pluginCodeName}EditorTarget";
+ await File.WriteAllTextAsync(newFlaxProjFile, JsonSerializer.Serialize(flaxPluginProjContents, typeof(ProjectInfo)), Encoding.UTF8);
+
+ // Format game settings
+ var gameSettingsPath = Path.Combine(newPluginPath, "Content", "GameSettings.json");
+ if (File.Exists(gameSettingsPath))
+ {
+ var contents = await File.ReadAllTextAsync(gameSettingsPath);
+ contents = contents.Replace("Example Plugin", pluginName);
+ contents = contents.Replace("\"CompanyName\": \"Flax\"", $"\"CompanyName\": \"{companyName}\"");
+ contents = contents.Replace("1.0", pluginVersion);
+ await File.WriteAllTextAsync(gameSettingsPath, contents, Encoding.UTF8);
+ }
+
+ // Rename source directories
+ var sourcePath = Path.Combine(newPluginPath, "Source");
+ var sourceDirectories = Directory.GetDirectories(sourcePath);
+ foreach (var directory in sourceDirectories)
+ {
+ var files = Directory.GetFiles(directory);
+ foreach (var file in files)
+ {
+ if (file.Contains("MyPluginEditor.cs"))
+ {
+ File.Delete(file);
+ continue;
+ }
+
+ var fileName = Path.GetFileName(file).Replace("ExamplePlugin", pluginCodeName);
+ var fileText = await File.ReadAllTextAsync(file);
+ fileText = fileText.Replace("ExamplePlugin", pluginCodeName);
+ if (file.Contains("MyPlugin.cs"))
+ {
+ fileName = "ExamplePlugin.cs";
+ fileText = fileText.Replace("MyPlugin", pluginCodeName);
+ fileText = fileText.Replace("My Plugin", pluginName);
+ fileText = fileText.Replace("Flax Engine", companyName);
+ fileText = fileText.Replace("new Version(1, 0)", $"new Version({pluginVersion.Trim().Replace(".", ", ")})");
+ }
+ await File.WriteAllTextAsync(file, fileText);
+ File.Move(file, Path.Combine(directory, fileName));
+ }
+
+ var newName = directory.Replace("ExamplePlugin", pluginCodeName);
+ Directory.Move(directory, newName);
+ }
+
+ // Rename targets
+ var targetFiles = Directory.GetFiles(sourcePath);
+ foreach (var file in targetFiles)
+ {
+ var fileText = await File.ReadAllTextAsync(file);
+ await File.WriteAllTextAsync(file, fileText.Replace("ExamplePlugin", pluginCodeName), Encoding.UTF8);
+ var newName = file.Replace("ExamplePlugin", pluginCodeName);
+ File.Move(file, newName);
+ }
+ Editor.Log($"Plugin project {pluginName} has successfully been created.");
+
+ await AddReferenceToProject(pluginName, pluginName);
+ MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK);
+ }
+
+ private async Task AddReferenceToProject(string pluginFolderName, string pluginName)
+ {
+ // Project flax config file
+ var flaxProjPath = Editor.GameProject.ProjectPath;
+ if (File.Exists(flaxProjPath))
+ {
+ var flaxProjContents = JsonSerializer.Deserialize(await File.ReadAllTextAsync(flaxProjPath));
+ var oldReferences = flaxProjContents.References;
+ var references = new List(oldReferences);
+ var newPath = $"$(ProjectPath)/Plugins/{pluginFolderName}/{pluginName}.flaxproj";
+ if (!references.Exists(x => string.Equals(x.Name, newPath)))
+ {
+ var newReference = new ProjectInfo.Reference
+ {
+ Name = newPath,
+ };
+ references.Add(newReference);
+ }
+ flaxProjContents.References = references.ToArray();
+ await File.WriteAllTextAsync(flaxProjPath, JsonSerializer.Serialize(flaxProjContents, typeof(ProjectInfo)), Encoding.UTF8);
+ }
+ }
+
private void OnPluginsChanged()
{
List toRemove = null;
@@ -299,19 +865,21 @@ namespace FlaxEditor.Windows
{
if (pluginType == null)
return null;
-
foreach (var e in _entries.Keys)
{
if (e.GetType() == pluginType && _entries.ContainsKey(e))
return _entries[e];
}
-
return null;
}
///
public override void OnDestroy()
{
+ if (_addPluginProjectButton != null)
+ _addPluginProjectButton.Clicked -= OnAddButtonClicked;
+ if (_cloneProjectButton != null)
+ _cloneProjectButton.Clicked -= OnCloneProjectButtonClicked;
PluginManager.PluginsChanged -= OnPluginsChanged;
base.OnDestroy();
diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp
index e292a0133..698f1ca8c 100644
--- a/Source/Engine/Content/Assets/VisualScript.cpp
+++ b/Source/Engine/Content/Assets/VisualScript.cpp
@@ -1251,7 +1251,7 @@ void VisualScriptExecutor::ProcessGroupFlow(Box* boxBase, Node* node, Value& val
boxBase = node->GetBox(3);
if (boxBase->HasConnection())
eatBox(node, boxBase->FirstConnection());
- Dictionary::Iterator it(dictionary, iteratorValue.Value.AsInt);
+ Dictionary::Iterator it(&dictionary, iteratorValue.Value.AsInt);
++it;
iteratorValue.Value.AsInt = it.Index();
}
@@ -1269,12 +1269,12 @@ void VisualScriptExecutor::ProcessGroupFlow(Box* boxBase, Node* node, Value& val
// Key
case 1:
if (iteratorIndex != scope->ReturnedValues.Count() && dictionaryIndex != scope->ReturnedValues.Count())
- value = Dictionary::Iterator(*scope->ReturnedValues[dictionaryIndex].Value.AsDictionary, scope->ReturnedValues[iteratorIndex].Value.AsInt)->Key;
+ value = Dictionary::Iterator(scope->ReturnedValues[dictionaryIndex].Value.AsDictionary, scope->ReturnedValues[iteratorIndex].Value.AsInt)->Key;
break;
// Value
case 2:
if (iteratorIndex != scope->ReturnedValues.Count() && dictionaryIndex != scope->ReturnedValues.Count())
- value = Dictionary::Iterator(*scope->ReturnedValues[dictionaryIndex].Value.AsDictionary, scope->ReturnedValues[iteratorIndex].Value.AsInt)->Value;
+ value = Dictionary::Iterator(scope->ReturnedValues[dictionaryIndex].Value.AsDictionary, scope->ReturnedValues[iteratorIndex].Value.AsInt)->Value;
break;
// Break
case 5:
diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h
index 3feae4e73..58117cf0a 100644
--- a/Source/Engine/Core/Collections/Array.h
+++ b/Source/Engine/Core/Collections/Array.h
@@ -938,12 +938,12 @@ public:
FORCE_INLINE bool IsEnd() const
{
- return _index == _array->Count();
+ return _index == _array->_count;
}
FORCE_INLINE bool IsNotEnd() const
{
- return _index != _array->Count();
+ return _index != _array->_count;
}
FORCE_INLINE T& operator*() const
@@ -975,7 +975,7 @@ public:
Iterator& operator++()
{
- if (_index != _array->Count())
+ if (_index != _array->_count)
_index++;
return *this;
}
@@ -983,7 +983,7 @@ public:
Iterator operator++(int)
{
Iterator temp = *this;
- if (_index != _array->Count())
+ if (_index != _array->_count)
_index++;
return temp;
}
diff --git a/Source/Engine/Core/Collections/ChunkedArray.h b/Source/Engine/Core/Collections/ChunkedArray.h
index 86473763b..d01711e38 100644
--- a/Source/Engine/Core/Collections/ChunkedArray.h
+++ b/Source/Engine/Core/Collections/ChunkedArray.h
@@ -95,7 +95,6 @@ public:
struct Iterator
{
friend ChunkedArray;
-
private:
ChunkedArray* _collection;
int32 _chunkIndex;
diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h
index 2327f3c24..f5ffb1bfa 100644
--- a/Source/Engine/Core/Collections/Dictionary.h
+++ b/Source/Engine/Core/Collections/Dictionary.h
@@ -237,22 +237,28 @@ public:
{
friend Dictionary;
private:
- Dictionary& _collection;
+ Dictionary* _collection;
int32 _index;
public:
- Iterator(Dictionary& collection, const int32 index)
+ Iterator(Dictionary* collection, const int32 index)
: _collection(collection)
, _index(index)
{
}
- Iterator(Dictionary const& collection, const int32 index)
- : _collection((Dictionary&)collection)
+ Iterator(Dictionary const* collection, const int32 index)
+ : _collection(const_cast(collection))
, _index(index)
{
}
+ Iterator()
+ : _collection(nullptr)
+ , _index(-1)
+ {
+ }
+
Iterator(const Iterator& i)
: _collection(i._collection)
, _index(i._index)
@@ -273,27 +279,27 @@ public:
FORCE_INLINE bool IsEnd() const
{
- return _index == _collection._size;
+ return _index == _collection->_size;
}
FORCE_INLINE bool IsNotEnd() const
{
- return _index != _collection._size;
+ return _index != _collection->_size;
}
FORCE_INLINE Bucket& operator*() const
{
- return _collection._allocation.Get()[_index];
+ return _collection->_allocation.Get()[_index];
}
FORCE_INLINE Bucket* operator->() const
{
- return &_collection._allocation.Get()[_index];
+ return &_collection->_allocation.Get()[_index];
}
FORCE_INLINE explicit operator bool() const
{
- return _index >= 0 && _index < _collection._size;
+ return _index >= 0 && _index < _collection->_size;
}
FORCE_INLINE bool operator!() const
@@ -308,7 +314,7 @@ public:
FORCE_INLINE bool operator!=(const Iterator& v) const
{
- return _index != v._index || &_collection != &v._collection;
+ return _index != v._index || _collection != v._collection;
}
Iterator& operator=(const Iterator& v)
@@ -320,10 +326,10 @@ public:
Iterator& operator++()
{
- const int32 capacity = _collection.Capacity();
+ const int32 capacity = _collection->_size;
if (_index != capacity)
{
- const Bucket* data = _collection._allocation.Get();
+ const Bucket* data = _collection->_allocation.Get();
do
{
_index++;
@@ -343,7 +349,7 @@ public:
{
if (_index > 0)
{
- const Bucket* data = _collection._allocation.Get();
+ const Bucket* data = _collection->_allocation.Get();
do
{
_index--;
@@ -633,7 +639,7 @@ public:
/// Iterator with key and value.
void Add(const Iterator& i)
{
- ASSERT(&i._collection != this && i);
+ ASSERT(i._collection != this && i);
const Bucket& bucket = *i;
Add(bucket.Key, bucket.Value);
}
@@ -667,7 +673,7 @@ public:
/// True if cannot remove item from the collection because cannot find it, otherwise false.
bool Remove(const Iterator& i)
{
- ASSERT(&i._collection == this);
+ ASSERT(i._collection == this);
if (i)
{
ASSERT(_allocation.Get()[i._index].IsOccupied());
@@ -711,7 +717,7 @@ public:
return End();
FindPositionResult pos;
FindPosition(key, pos);
- return pos.ObjectIndex != -1 ? Iterator(*this, pos.ObjectIndex) : End();
+ return pos.ObjectIndex != -1 ? Iterator(this, pos.ObjectIndex) : End();
}
///
@@ -812,38 +818,38 @@ public:
public:
Iterator Begin() const
{
- Iterator i(*this, -1);
+ Iterator i(this, -1);
++i;
return i;
}
Iterator End() const
{
- return Iterator(*this, _size);
+ return Iterator(this, _size);
}
Iterator begin()
{
- Iterator i(*this, -1);
+ Iterator i(this, -1);
++i;
return i;
}
FORCE_INLINE Iterator end()
{
- return Iterator(*this, _size);
+ return Iterator(this, _size);
}
const Iterator begin() const
{
- Iterator i(*this, -1);
+ Iterator i(this, -1);
++i;
return i;
}
FORCE_INLINE const Iterator end() const
{
- return Iterator(*this, _size);
+ return Iterator(this, _size);
}
protected:
diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h
index ad6f8ffc6..107e42e65 100644
--- a/Source/Engine/Core/Collections/HashSet.h
+++ b/Source/Engine/Core/Collections/HashSet.h
@@ -213,17 +213,17 @@ public:
{
friend HashSet;
private:
- HashSet& _collection;
+ HashSet* _collection;
int32 _index;
- Iterator(HashSet& collection, const int32 index)
+ Iterator(HashSet* collection, const int32 index)
: _collection(collection)
, _index(index)
{
}
- Iterator(HashSet const& collection, const int32 index)
- : _collection((HashSet&)collection)
+ Iterator(HashSet const* collection, const int32 index)
+ : _collection(const_cast(collection))
, _index(index)
{
}
@@ -244,27 +244,27 @@ public:
public:
FORCE_INLINE bool IsEnd() const
{
- return _index == _collection.Capacity();
+ return _index == _collection->_size;
}
FORCE_INLINE bool IsNotEnd() const
{
- return _index != _collection.Capacity();
+ return _index != _collection->_size;
}
FORCE_INLINE Bucket& operator*() const
{
- return _collection._allocation.Get()[_index];
+ return _collection->_allocation.Get()[_index];
}
FORCE_INLINE Bucket* operator->() const
{
- return &_collection._allocation.Get()[_index];
+ return &_collection->_allocation.Get()[_index];
}
FORCE_INLINE explicit operator bool() const
{
- return _index >= 0 && _index < _collection._size;
+ return _index >= 0 && _index < _collection->_size;
}
FORCE_INLINE bool operator !() const
@@ -274,12 +274,12 @@ public:
FORCE_INLINE bool operator==(const Iterator& v) const
{
- return _index == v._index && &_collection == &v._collection;
+ return _index == v._index && _collection == v._collection;
}
FORCE_INLINE bool operator!=(const Iterator& v) const
{
- return _index != v._index || &_collection != &v._collection;
+ return _index != v._index || _collection != v._collection;
}
Iterator& operator=(const Iterator& v)
@@ -291,10 +291,10 @@ public:
Iterator& operator++()
{
- const int32 capacity = _collection.Capacity();
+ const int32 capacity = _collection->_size;
if (_index != capacity)
{
- const Bucket* data = _collection._allocation.Get();
+ const Bucket* data = _collection->_allocation.Get();
do
{
_index++;
@@ -314,7 +314,7 @@ public:
{
if (_index > 0)
{
- const Bucket* data = _collection._allocation.Get();
+ const Bucket* data = _collection->_allocation.Get();
do
{
_index--;
@@ -464,7 +464,7 @@ public:
/// Iterator with item to add
void Add(const Iterator& i)
{
- ASSERT(&i._collection != this && i);
+ ASSERT(i._collection != this && i);
const Bucket& bucket = *i;
Add(bucket.Item);
}
@@ -498,7 +498,7 @@ public:
/// True if cannot remove item from the collection because cannot find it, otherwise false.
bool Remove(const Iterator& i)
{
- ASSERT(&i._collection == this);
+ ASSERT(i._collection == this);
if (i)
{
ASSERT(_allocation.Get()[i._index].IsOccupied());
@@ -523,7 +523,7 @@ public:
return End();
FindPositionResult pos;
FindPosition(item, pos);
- return pos.ObjectIndex != -1 ? Iterator(*this, pos.ObjectIndex) : End();
+ return pos.ObjectIndex != -1 ? Iterator(this, pos.ObjectIndex) : End();
}
///
@@ -559,38 +559,38 @@ public:
public:
Iterator Begin() const
{
- Iterator i(*this, -1);
+ Iterator i(this, -1);
++i;
return i;
}
Iterator End() const
{
- return Iterator(*this, _size);
+ return Iterator(this, _size);
}
Iterator begin()
{
- Iterator i(*this, -1);
+ Iterator i(this, -1);
++i;
return i;
}
FORCE_INLINE Iterator end()
{
- return Iterator(*this, _size);
+ return Iterator(this, _size);
}
const Iterator begin() const
{
- Iterator i(*this, -1);
+ Iterator i(this, -1);
++i;
return i;
}
FORCE_INLINE const Iterator end() const
{
- return Iterator(*this, _size);
+ return Iterator(this, _size);
}
protected:
diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp
index 97422b9e2..e8489f5b5 100644
--- a/Source/Engine/Debug/DebugDraw.cpp
+++ b/Source/Engine/Debug/DebugDraw.cpp
@@ -920,6 +920,11 @@ void DebugDraw::DrawActors(Actor** selectedActors, int32 selectedActorsCount, bo
}
}
+void DebugDraw::DrawRay(const Vector3& origin, const Vector3& direction, const Color& color, float duration, bool depthTest)
+{
+ DrawLine(origin, origin + direction, color, duration, depthTest);
+}
+
void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& color, float duration, bool depthTest)
{
const Float3 startF = start - Context->Origin, endF = end - Context->Origin;
diff --git a/Source/Engine/Debug/DebugDraw.cs b/Source/Engine/Debug/DebugDraw.cs
index af39ebeea..bc000e03d 100644
--- a/Source/Engine/Debug/DebugDraw.cs
+++ b/Source/Engine/Debug/DebugDraw.cs
@@ -31,6 +31,18 @@ namespace FlaxEngine
{
}
+ ///
+ /// Draws the line in a direction.
+ ///
+ /// The origin of the line.
+ /// The direction of the line.
+ /// The color.
+ /// The duration (in seconds). Use 0 to draw it only once.
+ /// If set to true depth test will be performed, otherwise depth will be ignored.
+ public static void DrawRay(Vector3 origin, Vector3 direction, Color color, float duration = 0.0f, bool depthTest = true)
+ {
+ }
+
///
/// Draws the line.
///
diff --git a/Source/Engine/Debug/DebugDraw.h b/Source/Engine/Debug/DebugDraw.h
index 8cc5452ed..a4f69678a 100644
--- a/Source/Engine/Debug/DebugDraw.h
+++ b/Source/Engine/Debug/DebugDraw.h
@@ -69,6 +69,16 @@ API_CLASS(Static) class FLAXENGINE_API DebugDraw
/// True if draw all debug shapes from scenes too or false if draw just from specified actor list.
API_FUNCTION() static void DrawActors(Actor** selectedActors, int32 selectedActorsCount, bool drawScenes);
+ ///
+ /// Draws the line in a direction.
+ ///
+ /// The origin of the line.
+ /// The direction of the line.
+ /// The color.
+ /// The duration (in seconds). Use 0 to draw it only once.
+ /// If set to true depth test will be performed, otherwise depth will be ignored.
+ API_FUNCTION() static void DrawRay(const Vector3& origin, const Vector3& direction, const Color& color, float duration = 0.0f, bool depthTest = true);
+
///
/// Draws the line.
///
@@ -604,6 +614,7 @@ API_CLASS(Static) class FLAXENGINE_API DebugDraw
API_FUNCTION() static void DrawText(const StringView& text, const Transform& transform, const Color& color, int32 size = 32, float duration = 0.0f);
};
+#define DEBUG_DRAW_RAY(origin, direction, color, duration, depthTest) DebugDraw::DrawRay(origin, direction, color, duration, depthTest)
#define DEBUG_DRAW_LINE(start, end, color, duration, depthTest) DebugDraw::DrawLine(start, end, color, duration, depthTest)
#define DEBUG_DRAW_LINES(lines, transform, color, duration, depthTest) DebugDraw::DrawLines(lines, transform, color, duration, depthTest)
#define DEBUG_DRAW_BEZIER(p1, p2, p3, p4, color, duration, depthTest) DebugDraw::DrawBezier(p1, p2, p3, p4, color, duration, depthTest)
diff --git a/Source/Engine/Engine/NativeInterop.Managed.cs b/Source/Engine/Engine/NativeInterop.Managed.cs
index e4dc449bc..f36798ece 100644
--- a/Source/Engine/Engine/NativeInterop.Managed.cs
+++ b/Source/Engine/Engine/NativeInterop.Managed.cs
@@ -371,7 +371,6 @@ namespace FlaxEngine.Interop
{
if (handle == IntPtr.Zero)
return;
-
ManagedHandlePool.FreeHandle(handle);
handle = IntPtr.Zero;
}
diff --git a/Source/Engine/Engine/NativeInterop.Marshallers.cs b/Source/Engine/Engine/NativeInterop.Marshallers.cs
index 3ea8a5faa..74d6e6bad 100644
--- a/Source/Engine/Engine/NativeInterop.Marshallers.cs
+++ b/Source/Engine/Engine/NativeInterop.Marshallers.cs
@@ -183,6 +183,9 @@ namespace FlaxEngine.Interop
public static void Free(IntPtr unmanaged) => ManagedHandleMarshaller.Free(unmanaged);
}
+#if FLAX_EDITOR
+ [HideInEditor]
+#endif
[CustomMarshaller(typeof(Array), MarshalMode.ManagedToUnmanagedIn, typeof(SystemArrayMarshaller.ManagedToNative))]
[CustomMarshaller(typeof(Array), MarshalMode.UnmanagedToManagedOut, typeof(SystemArrayMarshaller.ManagedToNative))]
[CustomMarshaller(typeof(Array), MarshalMode.ManagedToUnmanagedOut, typeof(SystemArrayMarshaller.NativeToManaged))]
@@ -251,6 +254,74 @@ namespace FlaxEngine.Interop
}
}
+#if FLAX_EDITOR
+ [HideInEditor]
+#endif
+ [CustomMarshaller(typeof(object[]), MarshalMode.ManagedToUnmanagedIn, typeof(SystemObjectArrayMarshaller.ManagedToNative))]
+ [CustomMarshaller(typeof(object[]), MarshalMode.UnmanagedToManagedOut, typeof(SystemObjectArrayMarshaller.ManagedToNative))]
+ [CustomMarshaller(typeof(object[]), MarshalMode.ManagedToUnmanagedOut, typeof(SystemObjectArrayMarshaller.NativeToManaged))]
+ [CustomMarshaller(typeof(object[]), MarshalMode.UnmanagedToManagedIn, typeof(SystemObjectArrayMarshaller.NativeToManaged))]
+ public static unsafe class SystemObjectArrayMarshaller
+ {
+#if FLAX_EDITOR
+ [HideInEditor]
+#endif
+ public struct ManagedToNative
+ {
+ ManagedHandle handle;
+
+ public void FromManaged(object[] managed)
+ {
+ if (managed != null)
+ {
+ var managedArray = NativeInterop.ManagedArrayToGCHandleWrappedArray(managed);
+ handle = ManagedHandle.Alloc(managedArray, GCHandleType.Weak);
+ }
+ }
+
+ public IntPtr ToUnmanaged()
+ {
+ return ManagedHandle.ToIntPtr(handle);
+ }
+
+ public void Free()
+ {
+ handle.Free();
+ }
+ }
+
+#if FLAX_EDITOR
+ [HideInEditor]
+#endif
+ public struct NativeToManaged
+ {
+ ManagedHandle handle;
+
+ public void FromUnmanaged(IntPtr unmanaged)
+ {
+ if (unmanaged == IntPtr.Zero)
+ return;
+ handle = ManagedHandle.FromIntPtr(unmanaged);
+ }
+
+ public object[] ToManaged()
+ {
+ object[] result = null;
+ if (handle.IsAllocated)
+ {
+ var managedArray = Unsafe.As(handle.Target);
+ result = NativeInterop.GCHandleArrayToManagedArray
API_PROPERTY() void SetLayerName(const StringView& value);
+ ///
+ /// Sets the name of the layer recursively for actor and for all underlying child actors.
+ ///
+ API_FUNCTION() void SetLayerNameRecursive(const StringView& value);
+
///
/// Determines whether this actor has any tag assigned.
///
@@ -136,6 +147,18 @@ public:
/// The tag to add.
API_FUNCTION() void AddTag(const Tag& tag);
+ ///
+ /// Adds a tag to the actor and for all underlying child actors.
+ ///
+ /// The tag to add.
+ API_FUNCTION() void AddTagRecursive(const Tag& tag);
+
+ ///
+ /// Removes a tag to the actor
+ ///
+ /// The tag to remove.
+ API_FUNCTION() void RemoveTag(const Tag& tag);
+
///
/// Gets the name of the tag.
/// [Deprecated in v1.5]
diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp
index dbd7ef045..194dbd9a9 100644
--- a/Source/Engine/Level/Actors/Spline.cpp
+++ b/Source/Engine/Level/Actors/Spline.cpp
@@ -484,8 +484,7 @@ namespace
void Spline::OnDebugDraw()
{
- const Color color = GetSplineColor();
- DrawSpline(this, color.AlphaMultiplied(0.7f), _transform, true);
+ DrawSpline(this, GetSplineColor().AlphaMultiplied(0.7f), _transform, true);
// Base
Actor::OnDebugDraw();
@@ -493,8 +492,7 @@ void Spline::OnDebugDraw()
void Spline::OnDebugDrawSelected()
{
- const Color color = GetSplineColor();
- DrawSpline(this, color.AlphaMultiplied(0.3f), _transform, false);
+ DrawSpline(this, Color::White, _transform, false);
// Base
Actor::OnDebugDrawSelected();
diff --git a/Source/Engine/Level/Actors/Spline.h b/Source/Engine/Level/Actors/Spline.h
index ea3cf8569..cd022890b 100644
--- a/Source/Engine/Level/Actors/Spline.h
+++ b/Source/Engine/Level/Actors/Spline.h
@@ -354,6 +354,7 @@ public:
protected:
#if USE_EDITOR
+ // Spline color getter for debug drawing, can be overriden by custom spline types.
virtual Color GetSplineColor()
{
return Color::White;
diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
index 5ea8ad0a4..e0df48fae 100644
--- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
@@ -1255,20 +1255,19 @@ void Prefab::SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, ArrayWaitForLoaded())
{
LOG(Warning, "Waiting for prefab asset load failed.");
continue;
}
+ // Sync only if prefab is used by this prefab (directly) and it has been captured before
const int32 nestedPrefabIndex = nestedPrefab->NestedPrefabs.Find(GetID());
if (nestedPrefabIndex != -1)
{
- if (nestedPrefab->SyncChangesInternal(allPrefabsInstancesData[nestedPrefabIndex]))
+ if (nestedPrefab->SyncChangesInternal(allPrefabsInstancesData[i]))
continue;
-
nestedPrefab->SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData);
-
ObjectsRemovalService::Flush();
}
}
diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp
index 9ef996869..9d91d0013 100644
--- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp
+++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp
@@ -331,6 +331,12 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupParticles(Box* box, Node* node
value = GET_PARTICLE_ATTRIBUTE(0, float);
break;
}
+ // Particle Scale
+ case 112:
+ {
+ value = GET_PARTICLE_ATTRIBUTE(0, Float3);
+ break;
+ }
// Effect Position
case 200:
{
diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp
index 905d1a11c..de142827c 100644
--- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp
+++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp
@@ -331,6 +331,10 @@ void ParticleEmitterGPUGenerator::ProcessGroupParticles(Box* box, Node* node, Va
case 111:
value = AccessParticleAttribute(node, TEXT("Radius"), ParticleAttribute::ValueTypes::Float, AccessMode::Read);
break;
+ // Particle Scale
+ case 112:
+ value = AccessParticleAttribute(node, TEXT("Scale"), ParticleAttribute::ValueTypes::Float3, AccessMode::Read);
+ break;
// Effect Position
case 200:
value = Value(VariantType::Float3, TEXT("EffectPosition"));
diff --git a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
index cfe259a72..f5946c226 100644
--- a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
+++ b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
@@ -269,13 +269,20 @@ public:
USE_ATTRIBUTE(Lifetime, Float, 1);
break;
}
- // Particle Mass
+ // Particle Radius
case GRAPH_NODE_MAKE_TYPE(14, 111):
{
node->UsesParticleData = true;
USE_ATTRIBUTE(Radius, Float, 0);
break;
}
+ // Particle Scale
+ case GRAPH_NODE_MAKE_TYPE(14, 112):
+ {
+ node->UsesParticleData = true;
+ USE_ATTRIBUTE(Scale, Float3, 0);
+ break;
+ }
// Random
case GRAPH_NODE_MAKE_TYPE(14, 208):
case GRAPH_NODE_MAKE_TYPE(14, 209):
diff --git a/Source/Engine/Physics/Actors/WheeledVehicle.cpp b/Source/Engine/Physics/Actors/WheeledVehicle.cpp
index 286f5b24e..afb5309ea 100644
--- a/Source/Engine/Physics/Actors/WheeledVehicle.cpp
+++ b/Source/Engine/Physics/Actors/WheeledVehicle.cpp
@@ -223,9 +223,10 @@ void WheeledVehicle::Setup()
return;
// Release previous
+ void* scene = GetPhysicsScene()->GetPhysicsScene();
if (_vehicle)
{
- PhysicsBackend::RemoveVehicle(GetPhysicsScene()->GetPhysicsScene(), this);
+ PhysicsBackend::RemoveVehicle(scene, this);
PhysicsBackend::DestroyVehicle(_vehicle, (int32)_driveTypeCurrent);
_vehicle = nullptr;
}
@@ -236,7 +237,7 @@ void WheeledVehicle::Setup()
if (!_vehicle)
return;
_driveTypeCurrent = _driveType;
- PhysicsBackend::AddVehicle(GetPhysicsScene()->GetPhysicsScene(), this);
+ PhysicsBackend::AddVehicle(scene, this);
PhysicsBackend::SetRigidDynamicActorSolverIterationCounts(_actor, 12, 4);
#else
LOG(Fatal, "Vehicles are not supported.");
@@ -358,8 +359,23 @@ void WheeledVehicle::OnColliderChanged(Collider* c)
{
RigidBody::OnColliderChanged(c);
- // Rebuild vehicle when someone adds/removed wheels
- Setup();
+ if (_useWheelsUpdates)
+ {
+ // Rebuild vehicle when someone adds/removed wheels
+ Setup();
+ }
+}
+
+void WheeledVehicle::OnActiveInTreeChanged()
+{
+ // Skip rebuilds from per-wheel OnColliderChanged when whole vehicle is toggled
+ _useWheelsUpdates = false;
+ RigidBody::OnActiveInTreeChanged();
+ _useWheelsUpdates = true;
+
+ // Perform whole rebuild when it gets activated
+ if (IsActiveInHierarchy())
+ Setup();
}
void WheeledVehicle::OnPhysicsSceneChanged(PhysicsScene* previous)
diff --git a/Source/Engine/Physics/Actors/WheeledVehicle.h b/Source/Engine/Physics/Actors/WheeledVehicle.h
index b478006d9..3a329edc4 100644
--- a/Source/Engine/Physics/Actors/WheeledVehicle.h
+++ b/Source/Engine/Physics/Actors/WheeledVehicle.h
@@ -329,6 +329,7 @@ private:
DifferentialSettings _differential;
GearboxSettings _gearbox;
bool _fixInvalidForwardDir = false; // [Deprecated on 13.06.2023, expires on 13.06.2025]
+ bool _useWheelsUpdates = true;
public:
///
@@ -484,6 +485,7 @@ public:
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
void OnColliderChanged(Collider* c) override;
+ void OnActiveInTreeChanged() override;
protected:
void OnPhysicsSceneChanged(PhysicsScene* previous) override;
diff --git a/Source/Engine/Physics/Colliders/CharacterController.cpp b/Source/Engine/Physics/Colliders/CharacterController.cpp
index 488acd6e4..59521e51c 100644
--- a/Source/Engine/Physics/Colliders/CharacterController.cpp
+++ b/Source/Engine/Physics/Colliders/CharacterController.cpp
@@ -8,6 +8,8 @@
#include "Engine/Serialization/Serialization.h"
#include "Engine/Engine/Time.h"
+#define CC_MIN_SIZE 0.001f
+
CharacterController::CharacterController(const SpawnParams& params)
: Collider(params)
, _controller(nullptr)
@@ -100,10 +102,9 @@ void CharacterController::SetStepOffset(float value)
{
const float scaling = _cachedScale.GetAbsolute().MaxValue();
const float contactOffset = Math::Max(_contactOffset, ZeroTolerance);
- const float minSize = 0.001f;
- const float height = Math::Max(Math::Abs(_height) * scaling, minSize);
- const float radius = Math::Max(Math::Abs(_radius) * scaling - contactOffset, minSize);
- PhysicsBackend::SetControllerStepOffset(_controller, Math::Min(value, height + radius * 2.0f - minSize));
+ const float height = Math::Max(Math::Abs(_height) * scaling, CC_MIN_SIZE);
+ const float radius = Math::Max(Math::Abs(_radius) * scaling - contactOffset, CC_MIN_SIZE);
+ PhysicsBackend::SetControllerStepOffset(_controller, Math::Min(value, height + radius * 2.0f - CC_MIN_SIZE));
}
}
@@ -175,9 +176,8 @@ CharacterController::CollisionFlags CharacterController::Move(const Vector3& dis
void CharacterController::DrawPhysicsDebug(RenderView& view)
{
const float scaling = _cachedScale.GetAbsolute().MaxValue();
- const float minSize = 0.001f;
- const float radius = Math::Max(Math::Abs(_radius) * scaling, minSize);
- const float height = Math::Max(Math::Abs(_height) * scaling, minSize);
+ const float radius = Math::Max(Math::Abs(_radius) * scaling, CC_MIN_SIZE);
+ const float height = Math::Max(Math::Abs(_height) * scaling, CC_MIN_SIZE);
const Vector3 position = _transform.LocalToWorld(_center);
if (view.Mode == ViewMode::PhysicsColliders)
DEBUG_DRAW_TUBE(position, Quaternion::Euler(90, 0, 0), radius, height, Color::LightYellow, 0, true);
@@ -188,9 +188,8 @@ void CharacterController::DrawPhysicsDebug(RenderView& view)
void CharacterController::OnDebugDrawSelected()
{
const float scaling = _cachedScale.GetAbsolute().MaxValue();
- const float minSize = 0.001f;
- const float radius = Math::Max(Math::Abs(_radius) * scaling, minSize);
- const float height = Math::Max(Math::Abs(_height) * scaling, minSize);
+ const float radius = Math::Max(Math::Abs(_radius) * scaling, CC_MIN_SIZE);
+ const float height = Math::Max(Math::Abs(_height) * scaling, CC_MIN_SIZE);
const Vector3 position = _transform.LocalToWorld(_center);
DEBUG_DRAW_WIRE_TUBE(position, Quaternion::Euler(90, 0, 0), radius, height, Color::GreenYellow, 0, false);
@@ -231,9 +230,8 @@ void CharacterController::UpdateSize() const
if (_controller)
{
const float scaling = _cachedScale.GetAbsolute().MaxValue();
- const float minSize = 0.001f;
- const float radius = Math::Max(Math::Abs(_radius) * scaling - Math::Max(_contactOffset, ZeroTolerance), minSize);
- const float height = Math::Max(Math::Abs(_height) * scaling, minSize);
+ const float radius = Math::Max(Math::Abs(_radius) * scaling - Math::Max(_contactOffset, ZeroTolerance), CC_MIN_SIZE);
+ const float height = Math::Max(Math::Abs(_height) * scaling, CC_MIN_SIZE);
PhysicsBackend::SetControllerSize(_controller, radius, height);
}
}
@@ -245,11 +243,12 @@ void CharacterController::CreateShape()
void CharacterController::UpdateBounds()
{
- void* actor = _shape ? PhysicsBackend::GetShapeActor(_shape) : nullptr;
- if (actor)
- PhysicsBackend::GetActorBounds(actor, _box);
- else
- _box = BoundingBox(_transform.Translation);
+ const float scaling = GetScale().GetAbsolute().MaxValue();
+ const float radius = Math::Max(Math::Abs(_radius) * scaling, CC_MIN_SIZE);
+ const float height = Math::Max(Math::Abs(_height) * scaling, CC_MIN_SIZE);
+ const Vector3 position = _transform.LocalToWorld(_center);
+ const Vector3 extent(radius, height * 0.5f + radius, radius);
+ _box = BoundingBox(position - extent, position + extent);
BoundingSphere::FromBox(_box, _sphere);
}
diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
index d4429bfcb..c8f3e0d50 100644
--- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
+++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
@@ -889,18 +889,42 @@ void* PhysicsBackend::CreateScene(const PhysicsSettings& settings)
PxSceneDesc sceneDesc(ToleranceScale);
sceneDesc.gravity = C2P(settings.DefaultGravity);
sceneDesc.flags |= PxSceneFlag::eENABLE_ACTIVE_ACTORS;
+ sceneDesc.flags |= PxSceneFlag::eENABLE_PCM;
if (!settings.DisableCCD)
sceneDesc.flags |= PxSceneFlag::eENABLE_CCD;
sceneDesc.simulationEventCallback = &scenePhysX->EventsCallback;
sceneDesc.filterShader = FilterShader;
sceneDesc.bounceThresholdVelocity = settings.BounceThresholdVelocity;
- sceneDesc.solverType = PxSolverType::ePGS;
+ switch (settings.SolverType)
+ {
+ case PhysicsSolverType::ProjectedGaussSeidelIterativeSolver:
+ sceneDesc.solverType = PxSolverType::ePGS;
+ break;
+ case PhysicsSolverType::TemporalGaussSeidelSolver:
+ sceneDesc.solverType = PxSolverType::eTGS;
+ break;
+ }
if (sceneDesc.cpuDispatcher == nullptr)
{
scenePhysX->CpuDispatcher = PxDefaultCpuDispatcherCreate(Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 1, 4));
CHECK_INIT(scenePhysX->CpuDispatcher, "PxDefaultCpuDispatcherCreate failed!");
sceneDesc.cpuDispatcher = scenePhysX->CpuDispatcher;
}
+ switch (settings.BroadPhaseType)
+ {
+ case PhysicsBroadPhaseType::SweepAndPrune:
+ sceneDesc.broadPhaseType = PxBroadPhaseType::eSAP;
+ break;
+ case PhysicsBroadPhaseType::MultiBoxPruning:
+ sceneDesc.broadPhaseType = PxBroadPhaseType::eMBP;
+ break;
+ case PhysicsBroadPhaseType::AutomaticBoxPruning:
+ sceneDesc.broadPhaseType = PxBroadPhaseType::eABP;
+ break;
+ case PhysicsBroadPhaseType::ParallelAutomaticBoxPruning:
+ sceneDesc.broadPhaseType = PxBroadPhaseType::ePABP;
+ break;
+ }
// Create scene
scenePhysX->Scene = PhysX->createScene(sceneDesc);
diff --git a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp
index d25beb2d3..4878750eb 100644
--- a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp
+++ b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp
@@ -11,8 +11,10 @@
namespace
{
- void ClearColliderFromCollection(PhysicsColliderActor* collider, Array& collection)
+ void ClearColliderFromCollection(const PhysicsColliderActor* collider, Array& collection)
{
+ if (collection.IsEmpty())
+ return;
for (int32 i = 0; i < collection.Count(); i++)
{
if (collection[i].First == collider || collection[i].Second == collider)
@@ -24,8 +26,10 @@ namespace
}
}
- void ClearColliderFromCollection(PhysicsColliderActor* collider, SimulationEventCallback::CollisionsPool& collection)
+ void ClearColliderFromCollection(const PhysicsColliderActor* collider, SimulationEventCallback::CollisionsPool& collection)
{
+ if (collection.IsEmpty())
+ return;
for (auto i = collection.Begin(); i.IsNotEnd(); ++i)
{
if (i->Key.First == collider || i->Key.Second == collider)
diff --git a/Source/Engine/Physics/Physics.cpp b/Source/Engine/Physics/Physics.cpp
index 1f9b242ad..7087b2e70 100644
--- a/Source/Engine/Physics/Physics.cpp
+++ b/Source/Engine/Physics/Physics.cpp
@@ -57,6 +57,8 @@ void PhysicsSettings::Deserialize(DeserializeStream& stream, ISerializeModifier*
DESERIALIZE(FrictionCombineMode);
DESERIALIZE(RestitutionCombineMode);
DESERIALIZE(DisableCCD);
+ DESERIALIZE(BroadPhaseType);
+ DESERIALIZE(SolverType);
DESERIALIZE(MaxDeltaTime);
DESERIALIZE(EnableSubstepping);
DESERIALIZE(SubstepDeltaTime);
diff --git a/Source/Engine/Physics/PhysicsSettings.h b/Source/Engine/Physics/PhysicsSettings.h
index 1533665e8..e43e96bc6 100644
--- a/Source/Engine/Physics/PhysicsSettings.h
+++ b/Source/Engine/Physics/PhysicsSettings.h
@@ -6,6 +6,50 @@
#include "Engine/Core/Math/Vector3.h"
#include "Types.h"
+///
+/// Broad phase algorithm used in the simulation.
+///
+///
+API_ENUM() enum class PhysicsBroadPhaseType
+{
+ ///
+ /// 3-axes sweep-and-prune. Good generic choice with great performance when many objects are sleeping.
+ ///
+ SweepAndPrune = 0,
+
+ ///
+ /// Alternative broad phase algorithm that does not suffer from the same performance issues as SAP when all objects are moving or when inserting large numbers of objects.
+ ///
+ MultiBoxPruning = 1,
+
+ ///
+ /// Revisited implementation of MBP, which automatically manages broad-phase regions.
+ ///
+ AutomaticBoxPruning = 2,
+
+ ///
+ /// Parallel implementation of ABP. It can often be the fastest (CPU) broadphase, but it can use more memory than ABP.
+ ///
+ ParallelAutomaticBoxPruning = 3,
+};
+
+///
+/// The type of solver used in the simulation.
+///
+///
+API_ENUM() enum class PhysicsSolverType
+{
+ ///
+ /// The iterative sequential impulse solver.
+ ///
+ ProjectedGaussSeidelIterativeSolver = 0,
+
+ ///
+ /// Non linear iterative solver. This kind of solver can lead to improved convergence and handle large mass ratios, long chains and jointed systems better. It is slightly more expensive than the default solver and can introduce more energy to correct joint and contact errors.
+ ///
+ TemporalGaussSeidelSolver = 1,
+};
+
///
/// Physics simulation settings container.
///
@@ -43,6 +87,18 @@ public:
API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Simulation\")")
bool DisableCCD = false;
+ ///
+ /// Broad phase algorithm to use in the simulation.
+ ///
+ API_FIELD(Attributes="EditorOrder(71), EditorDisplay(\"Simulation\")")
+ PhysicsBroadPhaseType BroadPhaseType = PhysicsBroadPhaseType::ParallelAutomaticBoxPruning;
+
+ ///
+ /// The solver type to use in the simulation.
+ ///
+ API_FIELD(Attributes="EditorOrder(72), EditorDisplay(\"Simulation\")")
+ PhysicsSolverType SolverType = PhysicsSolverType::ProjectedGaussSeidelIterativeSolver;
+
///
/// The maximum allowed delta time (in seconds) for the physics simulation step.
///
diff --git a/Source/Engine/Platform/Base/WindowBase.cpp b/Source/Engine/Platform/Base/WindowBase.cpp
index 5cff76361..49b4cc631 100644
--- a/Source/Engine/Platform/Base/WindowBase.cpp
+++ b/Source/Engine/Platform/Base/WindowBase.cpp
@@ -405,6 +405,7 @@ void WindowBase::OnResize(int32 width, int32 height)
_swapChain->Resize(width, height);
if (RenderTask)
RenderTask->Resize(width, height);
+ Resized({ static_cast(width), static_cast(height) });
INVOKE_EVENT_PARAMS_2(OnResize, &width, &height);
}
diff --git a/Source/Engine/Platform/Base/WindowBase.h b/Source/Engine/Platform/Base/WindowBase.h
index e4e7d2080..812359fb4 100644
--- a/Source/Engine/Platform/Base/WindowBase.h
+++ b/Source/Engine/Platform/Base/WindowBase.h
@@ -315,6 +315,11 @@ public:
///
Action Closed;
+ ///
+ /// Event fired when window gets resized.
+ ///
+ Delegate Resized;
+
///
/// Event fired when window gets focused.
///
diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.h b/Source/Engine/Scripting/ManagedCLR/MUtils.h
index 90feecab3..7b340e5c6 100644
--- a/Source/Engine/Scripting/ManagedCLR/MUtils.h
+++ b/Source/Engine/Scripting/ManagedCLR/MUtils.h
@@ -3,6 +3,7 @@
#pragma once
#include "MTypes.h"
+#include "MClass.h"
#include "MCore.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Types/DataContainer.h"
@@ -354,7 +355,7 @@ struct MConverter>
{
if (!klass)
return nullptr;
- MArray* result = MCore::Array::New(klass, data.Count());
+ MArray* result = MCore::Array::New(klass->GetElementClass(), data.Count());
MConverter converter;
converter.ToManagedArray(result, Span(data.Get(), data.Count()));
return (MObject*)result;
diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp
index f4ae5b1ec..1bcef50b8 100644
--- a/Source/Engine/Scripting/Runtime/DotNet.cpp
+++ b/Source/Engine/Scripting/Runtime/DotNet.cpp
@@ -365,8 +365,8 @@ MString* MCore::String::GetEmpty(MDomain* domain)
MString* MCore::String::New(const char* str, int32 length, MDomain* domain)
{
- static void* NewStringLengthPtr = GetStaticMethodPointer(TEXT("NewStringLength"));
- return (MString*)CallStaticMethod(NewStringLengthPtr, str, length);
+ static void* NewStringUTF8Ptr = GetStaticMethodPointer(TEXT("NewStringUTF8"));
+ return (MString*)CallStaticMethod(NewStringUTF8Ptr, str, length);
}
MString* MCore::String::New(const Char* str, int32 length, MDomain* domain)
diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs
index 2b49d97d4..68da64bf7 100644
--- a/Source/Engine/Scripting/Scripting.cs
+++ b/Source/Engine/Scripting/Scripting.cs
@@ -205,6 +205,13 @@ namespace FlaxEngine
internal static void AddDictionaryItem(IDictionary dictionary, object key, object value)
{
+ // TODO: more generic approach to properly add value that is of custom boxed type? (eg. via NativeInterop.MarshalToManaged)
+ if (value is ManagedArray managedArray)
+ {
+ var managedArrayHandle = ManagedHandle.Alloc(managedArray, GCHandleType.Normal);
+ value = NativeInterop.MarshalToManaged((IntPtr)managedArrayHandle, managedArray.ArrayType);
+ managedArrayHandle.Free();
+ }
dictionary.Add(key, value);
}
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index dfc249c6a..27423adb0 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -384,6 +384,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(BaseLOD);
SERIALIZE(LODCount);
SERIALIZE(TriangleReduction);
+ SERIALIZE(SloppyOptimization);
+ SERIALIZE(LODTargetError);
SERIALIZE(ImportMaterials);
SERIALIZE(ImportMaterialsAsInstances);
SERIALIZE(InstanceToImportAs);
@@ -427,6 +429,8 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(BaseLOD);
DESERIALIZE(LODCount);
DESERIALIZE(TriangleReduction);
+ DESERIALIZE(SloppyOptimization);
+ DESERIALIZE(LODTargetError);
DESERIALIZE(ImportMaterials);
DESERIALIZE(ImportMaterialsAsInstances);
DESERIALIZE(InstanceToImportAs);
@@ -1556,7 +1560,11 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
int32 dstMeshIndexCountTarget = int32(srcMeshIndexCount * triangleReduction) / 3 * 3;
Array indices;
indices.Resize(dstMeshIndexCountTarget);
- int32 dstMeshIndexCount = (int32)meshopt_simplifySloppy(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget);
+ int32 dstMeshIndexCount = {};
+ if (options.SloppyOptimization)
+ dstMeshIndexCount = (int32)meshopt_simplifySloppy(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget);
+ else
+ dstMeshIndexCount = (int32)meshopt_simplify(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError);
indices.Resize(dstMeshIndexCount);
if (dstMeshIndexCount == 0)
continue;
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h
index f6aaefa2b..d4ab4cedf 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.h
+++ b/Source/Engine/Tools/ModelTool/ModelTool.h
@@ -324,6 +324,12 @@ public:
// The target amount of triangles for the generated LOD (based on the higher LOD). Normalized to range 0-1. For instance 0.4 cuts the triangle count to 40%.
API_FIELD(Attributes="EditorOrder(1130), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry)), Limit(0, 1, 0.001f)")
float TriangleReduction = 0.5f;
+ // Whether to do a sloppy mesh optimization. This is faster but does not follow the topology of the original mesh.
+ API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry))")
+ bool SloppyOptimization = true;
+ // Only used if Sloppy is false. Target error is an approximate measure of the deviation from the original mesh using distance normalized to [0..1] range (e.g. 1e-2f means that simplifier will try to maintain the error to be below 1% of the mesh extents).
+ API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(SloppyOptimization), true), Limit(0.01f, 1, 0.001f)")
+ float LODTargetError = 0.1f;
public: // Materials
diff --git a/Source/Engine/UI/GUI/CanvasRootControl.cs b/Source/Engine/UI/GUI/CanvasRootControl.cs
index 408c46b0f..b4fa067e3 100644
--- a/Source/Engine/UI/GUI/CanvasRootControl.cs
+++ b/Source/Engine/UI/GUI/CanvasRootControl.cs
@@ -199,11 +199,11 @@ namespace FlaxEngine.GUI
// UI navigation
if (_canvas.ReceivesEvents)
{
- UpdateNavigation(deltaTime, _canvas.NavigationInputActionUp, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp);
- UpdateNavigation(deltaTime, _canvas.NavigationInputActionDown, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown);
- UpdateNavigation(deltaTime, _canvas.NavigationInputActionLeft, NavDirection.Left, ref _navigationHeldTimeLeft, ref _navigationRateTimeLeft);
- UpdateNavigation(deltaTime, _canvas.NavigationInputActionRight, NavDirection.Right, ref _navigationHeldTimeRight, ref _navigationRateTimeRight);
- UpdateNavigation(deltaTime, _canvas.NavigationInputActionSubmit, ref _navigationHeldTimeSubmit, ref _navigationRateTimeSubmit, SubmitFocused);
+ UpdateNavigation(deltaTime, _canvas.NavigateUp.Name, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp);
+ UpdateNavigation(deltaTime, _canvas.NavigateDown.Name, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown);
+ UpdateNavigation(deltaTime, _canvas.NavigateLeft.Name, NavDirection.Left, ref _navigationHeldTimeLeft, ref _navigationRateTimeLeft);
+ UpdateNavigation(deltaTime, _canvas.NavigateRight.Name, NavDirection.Right, ref _navigationHeldTimeRight, ref _navigationRateTimeRight);
+ UpdateNavigation(deltaTime, _canvas.NavigateSubmit.Name, ref _navigationHeldTimeSubmit, ref _navigationRateTimeSubmit, SubmitFocused);
}
else
{
diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs
new file mode 100644
index 000000000..d7324ae0e
--- /dev/null
+++ b/Source/Engine/UI/GUI/Common/Slider.cs
@@ -0,0 +1,365 @@
+using System;
+
+namespace FlaxEngine.GUI;
+
+///
+/// The slider control.
+///
+public class Slider : ContainerControl
+{
+ ///
+ /// The minimum value.
+ ///
+ protected float _minimum;
+
+ ///
+ /// The maximum value.
+ ///
+ protected float _maximum = 100f;
+
+ ///
+ /// Gets or sets the minimum value.
+ ///
+ [EditorOrder(20), Tooltip("The minimum value.")]
+ public float Minimum
+ {
+ get => _minimum;
+ set
+ {
+ if (value > _maximum)
+ throw new ArgumentOutOfRangeException();
+ if (WholeNumbers)
+ value = Mathf.RoundToInt(value);
+ _minimum = value;
+ if (Value < _minimum)
+ Value = _minimum;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum value.
+ ///
+ [EditorOrder(30), Tooltip("The maximum value.")]
+ public float Maximum
+ {
+ get => _maximum;
+ set
+ {
+ if (value < _minimum || Mathf.IsZero(value))
+ throw new ArgumentOutOfRangeException();
+ if (WholeNumbers)
+ value = Mathf.RoundToInt(value);
+ _maximum = value;
+ if (Value > _maximum)
+ Value = _maximum;
+ }
+ }
+
+ private float _value = 100f;
+ private Rectangle _thumbRect;
+ private float _thumbCenter;
+ private Float2 _thumbSize = new Float2(16, 16);
+ private bool _isSliding;
+
+ ///
+ /// Gets or sets the value (normalized to range 0-100).
+ ///
+ [EditorOrder(10), Tooltip("The current value.")]
+ public float Value
+ {
+ get => _value;
+ set
+ {
+ value = Mathf.Clamp(value, Minimum, Maximum);
+ if (WholeNumbers)
+ value = Mathf.RoundToInt(value);
+ if (!Mathf.NearEqual(value, _value))
+ {
+ _value = value;
+
+ // Update
+ UpdateThumb();
+ ValueChanged?.Invoke();
+ }
+ }
+ }
+
+ ///
+ /// The local position of the thumb center
+ ///
+ [HideInEditor]
+ public Float2 ThumbCenter => new(_thumbCenter, Height / 2);
+
+ ///
+ /// The local position of the beginning of the track.
+ ///
+ [HideInEditor]
+ public Float2 TrackBeginning => new(_thumbSize.X / 2, Height / 2);
+
+ ///
+ /// The local position of the end of the track.
+ ///
+ [HideInEditor]
+ public Float2 TrackEnd => new(Width - _thumbSize.X / 2, Height / 2);
+
+ ///
+ /// The height of the track.
+ ///
+ [EditorOrder(40), Tooltip("The track height.")]
+ public int TrackHeight { get; set; } = 2;
+
+ ///
+ /// The thumb size.
+ ///
+ [EditorOrder(41), Tooltip("The size of the thumb.")]
+ public Float2 ThumbSize {
+ get => _thumbSize;
+ set
+ {
+ _thumbSize = value;
+ UpdateThumb();
+ }
+ }
+
+ ///
+ /// Whether to fill the track.
+ ///
+ [EditorOrder(42), Tooltip("Fill the track.")]
+ public bool FillTrack = true;
+
+ ///
+ /// Whether to use whole numbers.
+ ///
+ [EditorOrder(43), Tooltip("Use whole numbers.")]
+ public bool WholeNumbers = false;
+
+ ///
+ /// The color of the slider track line
+ ///
+ [EditorDisplay("Track Style"), EditorOrder(2010), Tooltip("The color of the slider track line."), ExpandGroups]
+ public Color TrackLineColor { get; set; }
+
+ ///
+ /// The color of the slider fill track line
+ ///
+ [EditorDisplay("Track Style"), EditorOrder(2011), VisibleIf(nameof(FillTrack)), Tooltip("The color of the slider fill track line.")]
+ public Color TrackFillLineColor { get; set; }
+
+ ///
+ /// Gets the size of the track.
+ ///
+ private float TrackWidth => Width;
+
+ ///
+ /// Gets or sets the brush used for slider track drawing.
+ ///
+ [EditorDisplay("Track Style"), EditorOrder(2012), Tooltip("The brush used for slider track drawing.")]
+ public IBrush TrackBrush { get; set; }
+
+ ///
+ /// Gets or sets the brush used for slider fill track drawing.
+ ///
+ [EditorDisplay("Track Style"), EditorOrder(2013), VisibleIf(nameof(FillTrack)), Tooltip("The brush used for slider fill track drawing.")]
+ public IBrush FillTrackBrush { get; set; }
+
+ ///
+ /// The color of the slider thumb when it's not selected
+ ///
+ [EditorDisplay("Thumb Style"), EditorOrder(2030), Tooltip("The color of the slider thumb when it's not selected."), ExpandGroups]
+ public Color ThumbColor { get; set; }
+
+ ///
+ /// The color of the slider thumb when it's selected
+ ///
+ [EditorDisplay("Thumb Style"), EditorOrder(2031), Tooltip("The color of the slider thumb when it's selected.")]
+ public Color ThumbColorSelected { get; set; }
+
+ ///
+ /// Gets or sets the brush used for slider thumb drawing.
+ ///
+ [EditorDisplay("Thumb Style"), EditorOrder(2032), Tooltip("The brush of the slider thumb.")]
+ public IBrush ThumbBrush { get; set; }
+
+ ///
+ /// Gets a value indicating whether user is using a slider.
+ ///
+ [HideInEditor]
+ public bool IsSliding => _isSliding;
+
+ ///
+ /// Occurs when sliding starts.
+ ///
+ public event Action SlidingStart;
+
+ ///
+ /// Occurs when sliding ends.
+ ///
+ public event Action SlidingEnd;
+
+ ///
+ /// Occurs when value gets changed.
+ ///
+ public event Action ValueChanged;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Slider()
+ : this(120, 30)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The width.
+ /// The height.
+ public Slider(float width, float height)
+ : base(0, 0, width, height)
+ {
+ var style = Style.Current;
+ TrackLineColor = style.BackgroundHighlighted;
+ TrackFillLineColor = style.LightBackground;
+ ThumbColor = style.BackgroundNormal;
+ ThumbColorSelected = style.BackgroundSelected;
+ UpdateThumb();
+ }
+
+ private void UpdateThumb()
+ {
+ // Cache data
+ float trackSize = TrackWidth;
+ float range = Maximum - Minimum;
+ float pixelRange = trackSize - _thumbSize.X;
+ float perc = (_value - Minimum) / range;
+ float thumbPosition = (int)(perc * pixelRange);
+ _thumbCenter = thumbPosition + _thumbSize.X / 2;
+ _thumbRect = new Rectangle(thumbPosition, (Height - _thumbSize.Y) / 2, _thumbSize.X, _thumbSize.Y);
+ }
+
+ private void EndSliding()
+ {
+ _isSliding = false;
+ EndMouseCapture();
+ SlidingEnd?.Invoke();
+ }
+
+ ///
+ public override void Draw()
+ {
+ base.Draw();
+
+ // Draw track line
+ //var lineRect = new Rectangle(4, (Height - TrackHeight) / 2, Width - 8, TrackHeight);
+ var lineRect = new Rectangle(_thumbSize.X / 2, (Height - TrackHeight) / 2, Width - _thumbSize.X, TrackHeight);
+ if (TrackBrush != null)
+ TrackBrush.Draw(lineRect, TrackLineColor);
+ else
+ Render2D.FillRectangle(lineRect, TrackLineColor);
+
+ // Draw track fill
+ if (FillTrack)
+ {
+ var fillLineRect = new Rectangle(_thumbSize.X / 2, (Height - TrackHeight - 2) / 2, Width - (Width - _thumbCenter) - _thumbSize.X / 2, TrackHeight + 2);
+ Render2D.PushClip(ref fillLineRect);
+ if (FillTrackBrush != null)
+ FillTrackBrush.Draw(lineRect, TrackFillLineColor);
+ else
+ Render2D.FillRectangle(lineRect, TrackFillLineColor);
+ Render2D.PopClip();
+ }
+
+ // Draw thumb
+ var thumbColor = _isSliding ? ThumbColorSelected : ThumbColor;
+ if (ThumbBrush != null)
+ ThumbBrush.Draw(_thumbRect, thumbColor);
+ else
+ Render2D.FillRectangle(_thumbRect, thumbColor);
+ }
+
+ ///
+ public override void OnLostFocus()
+ {
+ if (_isSliding)
+ {
+ EndSliding();
+ }
+
+ base.OnLostFocus();
+ }
+
+ ///
+ public override bool OnMouseDown(Float2 location, MouseButton button)
+ {
+ if (button == MouseButton.Left)
+ {
+ Focus();
+ float mousePosition = location.X;
+
+ if (_thumbRect.Contains(ref location))
+ {
+ // Start sliding
+ _isSliding = true;
+ StartMouseCapture();
+ SlidingStart?.Invoke();
+ return true;
+ }
+ else
+ {
+ // Click change
+ Value += (mousePosition < _thumbCenter ? -1 : 1) * 10;
+ }
+ }
+
+ return base.OnMouseDown(location, button);
+ }
+
+ ///
+ public override void OnMouseMove(Float2 location)
+ {
+ if (_isSliding)
+ {
+ // Update sliding
+ var slidePosition = location + Root.TrackingMouseOffset;
+ Value = Mathf.Remap(slidePosition.X, 4, TrackWidth - 4, Minimum, Maximum);
+ }
+ else
+ {
+ base.OnMouseMove(location);
+ }
+ }
+
+ ///
+ public override bool OnMouseUp(Float2 location, MouseButton button)
+ {
+ if (button == MouseButton.Left && _isSliding)
+ {
+ EndSliding();
+ return true;
+ }
+
+ return base.OnMouseUp(location, button);
+ }
+
+ ///
+ public override void OnEndMouseCapture()
+ {
+ // Check if was sliding
+ if (_isSliding)
+ {
+ EndSliding();
+ }
+ else
+ {
+ base.OnEndMouseCapture();
+ }
+ }
+
+ ///
+ protected override void OnSizeChanged()
+ {
+ base.OnSizeChanged();
+
+ UpdateThumb();
+ }
+}
diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs
index 87fc9506f..8cb43af62 100644
--- a/Source/Engine/UI/UICanvas.cs
+++ b/Source/Engine/UI/UICanvas.cs
@@ -263,39 +263,39 @@ namespace FlaxEngine
public float NavigationInputRepeatRate { get; set; } = 0.05f;
///
- /// The name of the input action for performing UI navigation Up (from Input Settings).
+ /// The input action for performing UI navigation Up (from Input Settings).
///
[EditorOrder(510), EditorDisplay("Navigation", "Navigate Up")]
- [Tooltip("The name of the input action for performing UI navigation Up (from Input Settings).")]
- public string NavigationInputActionUp { get; set; } = "NavigateUp";
+ [Tooltip("The input action for performing UI navigation Up (from Input Settings).")]
+ public InputEvent NavigateUp { get; set; } = new InputEvent("NavigateUp");
///
- /// The name of the input action for performing UI navigation Down (from Input Settings).
+ /// The input action for performing UI navigation Down (from Input Settings).
///
[EditorOrder(520), EditorDisplay("Navigation", "Navigate Down")]
- [Tooltip("The name of the input action for performing UI navigation Down (from Input Settings).")]
- public string NavigationInputActionDown { get; set; } = "NavigateDown";
+ [Tooltip("The input action for performing UI navigation Down (from Input Settings).")]
+ public InputEvent NavigateDown { get; set; } = new InputEvent("NavigateDown");
///
- /// The name of the input action for performing UI navigation Left (from Input Settings).
+ /// The input action for performing UI navigation Left (from Input Settings).
///
[EditorOrder(530), EditorDisplay("Navigation", "Navigate Left")]
- [Tooltip("The name of the input action for performing UI navigation Left (from Input Settings).")]
- public string NavigationInputActionLeft { get; set; } = "NavigateLeft";
+ [Tooltip("The input action for performing UI navigation Left (from Input Settings).")]
+ public InputEvent NavigateLeft { get; set; } = new InputEvent("NavigateLeft");
///
- /// The name of the input action for performing UI navigation Right (from Input Settings).
+ /// The input action for performing UI navigation Right (from Input Settings).
///
[EditorOrder(540), EditorDisplay("Navigation", "Navigate Right")]
- [Tooltip("The name of the input action for performing UI navigation Right (from Input Settings).")]
- public string NavigationInputActionRight { get; set; } = "NavigateRight";
+ [Tooltip("The input action for performing UI navigation Right (from Input Settings).")]
+ public InputEvent NavigateRight { get; set; } = new InputEvent("NavigateRight");
///
- /// The name of the input action for performing UI navigation Submit (from Input Settings).
+ /// The input action for performing UI navigation Submit (from Input Settings).
///
- [EditorOrder(540), EditorDisplay("Navigation", "Navigate Submit")]
- [Tooltip("The name of the input action for performing UI navigation Submit (from Input Settings).")]
- public string NavigationInputActionSubmit { get; set; } = "NavigateSubmit";
+ [EditorOrder(550), EditorDisplay("Navigation", "Navigate Submit")]
+ [Tooltip("The input action for performing UI navigation Submit (from Input Settings).")]
+ public InputEvent NavigateSubmit { get; set; } = new InputEvent("NavigateSubmit");
#endregion
@@ -620,14 +620,36 @@ namespace FlaxEngine
jsonWriter.WriteValue(NavigationInputRepeatDelay);
jsonWriter.WritePropertyName("NavigationInputRepeatRate");
jsonWriter.WriteValue(NavigationInputRepeatRate);
- jsonWriter.WritePropertyName("NavigationInputActionUp");
- jsonWriter.WriteValue(NavigationInputActionUp);
- jsonWriter.WritePropertyName("NavigationInputActionDown");
- jsonWriter.WriteValue(NavigationInputActionDown);
- jsonWriter.WritePropertyName("NavigationInputActionLeft");
- jsonWriter.WriteValue(NavigationInputActionLeft);
- jsonWriter.WritePropertyName("NavigationInputActionRight");
- jsonWriter.WriteValue(NavigationInputActionRight);
+
+ jsonWriter.WritePropertyName("NavigateUp");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateUp.Name);
+ jsonWriter.WriteEndObject();
+
+ jsonWriter.WritePropertyName("NavigateDown");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateDown.Name);
+ jsonWriter.WriteEndObject();
+
+ jsonWriter.WritePropertyName("NavigateLeft");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateLeft.Name);
+ jsonWriter.WriteEndObject();
+
+ jsonWriter.WritePropertyName("NavigateRight");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateRight.Name);
+ jsonWriter.WriteEndObject();
+
+ jsonWriter.WritePropertyName("NavigateSubmit");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateSubmit.Name);
+ jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
}
@@ -713,25 +735,45 @@ namespace FlaxEngine
jsonWriter.WritePropertyName("NavigationInputRepeatRate");
jsonWriter.WriteValue(NavigationInputRepeatRate);
}
- if (NavigationInputActionUp != other.NavigationInputActionUp)
+ if (NavigateUp.Name != other.NavigateUp.Name)
{
- jsonWriter.WritePropertyName("NavigationInputActionUp");
- jsonWriter.WriteValue(NavigationInputActionUp);
+ jsonWriter.WritePropertyName("NavigateUp");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateUp.Name);
+ jsonWriter.WriteEndObject();
}
- if (NavigationInputActionDown != other.NavigationInputActionDown)
+ if (NavigateDown.Name != other.NavigateDown.Name)
{
- jsonWriter.WritePropertyName("NavigationInputActionDown");
- jsonWriter.WriteValue(NavigationInputActionDown);
+ jsonWriter.WritePropertyName("NavigateDown");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateDown.Name);
+ jsonWriter.WriteEndObject();
}
- if (NavigationInputActionLeft != other.NavigationInputActionLeft)
+ if (NavigateLeft.Name != other.NavigateLeft.Name)
{
- jsonWriter.WritePropertyName("NavigationInputActionLeft");
- jsonWriter.WriteValue(NavigationInputActionLeft);
+ jsonWriter.WritePropertyName("NavigateLeft");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateLeft.Name);
+ jsonWriter.WriteEndObject();
}
- if (NavigationInputActionRight != other.NavigationInputActionRight)
+ if (NavigateRight.Name != other.NavigateRight.Name)
{
- jsonWriter.WritePropertyName("NavigationInputActionRight");
- jsonWriter.WriteValue(NavigationInputActionRight);
+ jsonWriter.WritePropertyName("NavigateRight");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateRight.Name);
+ jsonWriter.WriteEndObject();
+ }
+ if (NavigateSubmit.Name != other.NavigateSubmit.Name)
+ {
+ jsonWriter.WritePropertyName("NavigateSubmit");
+ jsonWriter.WriteStartObject();
+ jsonWriter.WritePropertyName("Name");
+ jsonWriter.WriteValue(NavigateSubmit.Name);
+ jsonWriter.WriteEndObject();
}
jsonWriter.WriteEndObject();
diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp
index ca4495f67..1cee5d46e 100644
--- a/Source/Engine/Visject/VisjectGraph.cpp
+++ b/Source/Engine/Visject/VisjectGraph.cpp
@@ -1279,16 +1279,16 @@ void VisjectExecutor::ProcessGroupParticles(Box* box, Node* node, Value& value)
// Random Float Range
case 213:
{
- auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat;
- auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat;
+ auto a = (float)tryGetValue(node->TryGetBox(1), node->Values[0]);
+ auto b = (float)tryGetValue(node->TryGetBox(2), node->Values[1]);
value = Math::Lerp(a, b, RAND);
break;
}
// Random Vector2 Range
case 214:
{
- auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat2();
- auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat2();
+ auto a = (Float2)tryGetValue(node->TryGetBox(1), node->Values[0]);
+ auto b = (Float2)tryGetValue(node->TryGetBox(2), node->Values[1]);
value = Float2(
Math::Lerp(a.X, b.X, RAND),
Math::Lerp(a.Y, b.Y, RAND)
@@ -1298,8 +1298,8 @@ void VisjectExecutor::ProcessGroupParticles(Box* box, Node* node, Value& value)
// Random Vector3 Range
case 215:
{
- auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat3();
- auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat3();
+ auto a = (Float3)tryGetValue(node->TryGetBox(1), node->Values[0]);
+ auto b = (Float3)tryGetValue(node->TryGetBox(2), node->Values[1]);
value = Float3(
Math::Lerp(a.X, b.X, RAND),
Math::Lerp(a.Y, b.Y, RAND),
@@ -1310,8 +1310,8 @@ void VisjectExecutor::ProcessGroupParticles(Box* box, Node* node, Value& value)
// Random Vector4 Range
case 216:
{
- auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat4();
- auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat4();
+ auto a = (Float4)tryGetValue(node->TryGetBox(1), node->Values[0]);
+ auto b = (Float4)tryGetValue(node->TryGetBox(2), node->Values[1]);
value = Float4(
Math::Lerp(a.X, b.X, RAND),
Math::Lerp(a.Y, b.Y, RAND),
diff --git a/Source/Platforms/Android/Binaries/Project/app/build.gradle b/Source/Platforms/Android/Binaries/Project/app/build.gradle
index 0ad5776b5..2f5da0a56 100644
--- a/Source/Platforms/Android/Binaries/Project/app/build.gradle
+++ b/Source/Platforms/Android/Binaries/Project/app/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
+ namespace "${PackageName}"
defaultConfig {
applicationId "${PackageName}"
minSdkVersion 24
diff --git a/Source/Platforms/Android/Binaries/Project/build.gradle b/Source/Platforms/Android/Binaries/Project/build.gradle
index 31d9237bc..ff4610757 100644
--- a/Source/Platforms/Android/Binaries/Project/build.gradle
+++ b/Source/Platforms/Android/Binaries/Project/build.gradle
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath 'com.android.tools.build:gradle:8.1.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/Source/Platforms/Android/Binaries/Project/gradle.properties b/Source/Platforms/Android/Binaries/Project/gradle.properties
index 7be81848a..f38a936bd 100644
--- a/Source/Platforms/Android/Binaries/Project/gradle.properties
+++ b/Source/Platforms/Android/Binaries/Project/gradle.properties
@@ -9,5 +9,4 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-# Default value: -Xmx10248m -XX:MaxPermSize=256m
-org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
diff --git a/Source/Platforms/Android/Binaries/Project/gradle/wrapper/gradle-wrapper.properties b/Source/Platforms/Android/Binaries/Project/gradle/wrapper/gradle-wrapper.properties
index 483089527..ee1048a44 100644
--- a/Source/Platforms/Android/Binaries/Project/gradle/wrapper/gradle-wrapper.properties
+++ b/Source/Platforms/Android/Binaries/Project/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
index f399dd67c..b825f5ac1 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
@@ -521,7 +521,7 @@ namespace Flax.Build.Bindings
returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemTypeMarshaller))";
else if (returnValueType == "CultureInfo")
returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.CultureInfoMarshaller))";
- else if (functionInfo.ReturnType.Type == "Variant")
+ else if (functionInfo.ReturnType.Type == "Variant") // object
returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.ManagedHandleMarshaller))";
else if (FindApiTypeInfo(buildData, functionInfo.ReturnType, caller)?.IsInterface ?? false)
{
@@ -530,6 +530,8 @@ namespace Flax.Build.Bindings
}
else if (functionInfo.ReturnType.Type == "MonoArray" || functionInfo.ReturnType.Type == "MArray")
returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemArrayMarshaller))";
+ else if (returnValueType == "object[]")
+ returnMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemObjectArrayMarshaller))";
else if (functionInfo.ReturnType.Type == "Array" || functionInfo.ReturnType.Type == "Span" || functionInfo.ReturnType.Type == "DataContainer" || functionInfo.ReturnType.Type == "BytesContainer" || returnNativeType == "Array")
returnMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = nameof(__returnCount))";
else if (functionInfo.ReturnType.Type == "Dictionary")
@@ -579,6 +581,8 @@ namespace Flax.Build.Bindings
parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.ManagedHandleMarshaller))";
else if (parameterInfo.Type.Type == "MonoArray" || parameterInfo.Type.Type == "MArray")
parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemArrayMarshaller))";
+ 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"))})";
else if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer" || nativeType == "Array")