From 846065007ba9eb94c53fd1355b62f24f6a048d50 Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 22:49:13 +0300 Subject: [PATCH 01/87] Make visject inputboxes uniform size of 50 pixels --- Source/Editor/Surface/Elements/InputBox.cs | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/Editor/Surface/Elements/InputBox.cs b/Source/Editor/Surface/Elements/InputBox.cs index 9861ebacd..e52762298 100644 --- a/Source/Editor/Surface/Elements/InputBox.cs +++ b/Source/Editor/Surface/Elements/InputBox.cs @@ -120,7 +120,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = IntegerValue.Get(box.ParentNode, box.Archetype, box.Value); - var width = 40; + var width = 50; var control = new IntValueBox(value, bounds.X, bounds.Y, width + 12, int.MinValue, int.MaxValue, 0.01f) { Height = bounds.Height, @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = UnsignedIntegerValue.Get(box.ParentNode, box.Archetype, box.Value); - var width = 40; + var width = 50; var control = new UIntValueBox(value, bounds.X, bounds.Y, width + 12, uint.MinValue, uint.MaxValue, 0.01f) { Height = bounds.Height, @@ -212,7 +212,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = FloatValue.Get(box.ParentNode, box.Archetype, box.Value); - var width = 40; + var width = 50; var control = new FloatValueBox(value, bounds.X, bounds.Y, width + 12, float.MinValue, float.MaxValue, 0.01f) { Height = bounds.Height, @@ -303,7 +303,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 2 - 2, bounds.Height) { ClipChildren = false, @@ -377,7 +377,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -460,7 +460,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 4 - 2, bounds.Height) { ClipChildren = false, @@ -553,7 +553,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 2 - 2, bounds.Height) { ClipChildren = false, @@ -627,7 +627,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -710,7 +710,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 4 - 2, bounds.Height) { ClipChildren = false, @@ -803,7 +803,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 2 - 2, bounds.Height) { ClipChildren = false, @@ -877,7 +877,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -960,7 +960,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 4 - 2, bounds.Height) { ClipChildren = false, @@ -1053,7 +1053,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box).EulerAngles; - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, From 4bacceb19f5e873d4fb3420ef619cfca64793851 Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 22:49:30 +0300 Subject: [PATCH 02/87] Make comparisons automatically expand if needed so it contains the inputs --- .../Editor/Surface/Archetypes/Comparisons.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index bcf0159d2..22adea949 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -20,6 +20,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = id, Title = title, + Create = (id, context, arch, groupArch) => new ComparisonNode(id, context, arch, groupArch), Description = desc, Flags = NodeFlags.AllGraphs, AlternativeTitles = altTitles, @@ -44,6 +45,28 @@ namespace FlaxEditor.Surface.Archetypes }; } + private class ComparisonNode : SurfaceNode + { + public ComparisonNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + base.OnSurfaceLoaded(action); + + ResizeAuto(); + } + + public override void ConnectionTick(Box box) + { + base.ConnectionTick(box); + + ResizeAuto(); + } + } + private class SwitchOnEnumNode : SurfaceNode { public SwitchOnEnumNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) @@ -181,6 +204,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Switch On Bool", + Create = (id, context, arch, groupArch) => new ComparisonNode(id, context, arch, groupArch), AlternativeTitles = new[] { "if", "switch" }, Description = "Returns one of the input values based on the condition value", Flags = NodeFlags.AllGraphs, From b714668f4cd39c30f37695bde603ff1ff8fa7a0f Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 17 Dec 2025 18:54:37 +0100 Subject: [PATCH 03/87] improve visject graph readability - Separate size for boxes (the circle thingie to connect to) and box rows - Smaller boxes (circle thingie) - Smaller footer - Smaller and color header - Left align header text - Replace background with solid color - Adjust node `ArchetypeColor`s (former `FooterColor`) to keep header title readable over colored header --- .../Editor/Surface/Archetypes/BehaviorTree.cs | 6 ++--- Source/Editor/Surface/Archetypes/Function.cs | 2 +- Source/Editor/Surface/Archetypes/Textures.cs | 2 +- Source/Editor/Surface/Archetypes/Tools.cs | 2 +- Source/Editor/Surface/Constants.cs | 15 +++++++++---- Source/Editor/Surface/Elements/Box.cs | 2 +- Source/Editor/Surface/NodeElementArchetype.cs | 4 ++-- Source/Editor/Surface/NodeFactory.cs | 22 +++++++++---------- Source/Editor/Surface/SurfaceNode.cs | 16 +++++++++----- Source/Editor/Surface/SurfaceStyle.cs | 8 ++++++- Source/Editor/Surface/VisjectSurface.Draw.cs | 8 ++++++- 11 files changed, 56 insertions(+), 31 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 0b4b3f713..9c9325177 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -415,8 +415,8 @@ namespace FlaxEditor.Surface.Archetypes // Setup boxes _input = (InputBox)GetBox(0); _output = (OutputBox)GetBox(1); - _input.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxSize * -0.5f); - _output.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxSize * 0.5f); + _input.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxRowHeight * -0.5f); + _output.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxRowHeight * 0.5f); // Setup node type and data var flagsRoot = NodeFlags.NoRemove | NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaPaste; @@ -667,7 +667,7 @@ namespace FlaxEditor.Surface.Archetypes } } - protected override Color FooterColor => Color.Transparent; + protected override Color ArchetypeColor => Color.Transparent; protected override Float2 CalculateNodeSize(float width, float height) { diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 79d3948bf..6410b51e7 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -631,7 +631,7 @@ namespace FlaxEditor.Surface.Archetypes } /// - protected override Color FooterColor => new Color(200, 11, 112); + protected override Color ArchetypeColor => new Color(200, 11, 112); /// public override void OnLoaded(SurfaceNodeActions action) diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 56f8154d7..c1132e947 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -204,7 +204,7 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(3, "Max Steps", true, typeof(float), 3, 2), NodeElementArchetype.Factory.Input(4, "Heightmap Texture", true, typeof(FlaxEngine.Object), 4), NodeElementArchetype.Factory.Output(0, "Parallax UVs", typeof(Float2), 5), - NodeElementArchetype.Factory.Text(Surface.Constants.BoxSize + 4, 5 * Surface.Constants.LayoutOffsetY, "Channel"), + NodeElementArchetype.Factory.Text(Surface.Constants.BoxRowHeight + 4, 5 * Surface.Constants.LayoutOffsetY, "Channel"), NodeElementArchetype.Factory.ComboBox(70, 5 * Surface.Constants.LayoutOffsetY, 50, 3, new[] { "R", diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index aacebd189..3d0a8cd0c 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1058,7 +1058,7 @@ namespace FlaxEditor.Surface.Archetypes internal class RerouteNode : SurfaceNode, IConnectionInstigator { - internal static readonly Float2 DefaultSize = new Float2(FlaxEditor.Surface.Constants.BoxSize); + internal static readonly Float2 DefaultSize = new Float2(FlaxEditor.Surface.Constants.BoxRowHeight); private Rectangle _localBounds; private InputBox _input; private OutputBox _output; diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 0f3dca783..3310351bf 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -23,12 +23,14 @@ namespace FlaxEditor.Surface /// /// The node header height. /// - public const float NodeHeaderSize = 28.0f; + public const float NodeHeaderSize = 20.0f; + + public const float NodeHeaderTextScale = 0.65f; /// /// The node footer height. /// - public const float NodeFooterSize = 4.0f; + public const float NodeFooterSize = 2.0f; /// /// The node left margin. @@ -45,14 +47,19 @@ namespace FlaxEditor.Surface /// public const float BoxOffsetX = 2.0f; + /// + /// The width of the row that is started by a box. + /// + public const float BoxRowHeight = 18.0f; + /// /// The box size (with and height). /// - public const float BoxSize = 20.0f; + public const float BoxSize = 12.0f; /// /// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent. /// - public const float LayoutOffsetY = 20.0f; + public const float LayoutOffsetY = 22.0f; } } diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index 964b3cc69..08023ac93 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -196,7 +196,7 @@ namespace FlaxEditor.Surface.Elements /// protected Box(SurfaceNode parentNode, NodeElementArchetype archetype, Float2 location) - : base(parentNode, archetype, location, new Float2(Constants.BoxSize), false) + : base(parentNode, archetype, location, new Float2(Constants.BoxRowHeight), false) { _currentType = DefaultType; _isSingle = Archetype.Single; diff --git a/Source/Editor/Surface/NodeElementArchetype.cs b/Source/Editor/Surface/NodeElementArchetype.cs index f5a23d411..1a9d4ae74 100644 --- a/Source/Editor/Surface/NodeElementArchetype.cs +++ b/Source/Editor/Surface/NodeElementArchetype.cs @@ -157,7 +157,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxSize + Constants.BoxOffsetX, + Constants.NodeMarginX - Constants.BoxRowHeight + Constants.BoxOffsetX, Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, @@ -182,7 +182,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxSize + Constants.BoxOffsetX, + Constants.NodeMarginX - Constants.BoxRowHeight + Constants.BoxOffsetX, Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, diff --git a/Source/Editor/Surface/NodeFactory.cs b/Source/Editor/Surface/NodeFactory.cs index 7c85cb449..8448c37a3 100644 --- a/Source/Editor/Surface/NodeFactory.cs +++ b/Source/Editor/Surface/NodeFactory.cs @@ -60,56 +60,56 @@ namespace FlaxEditor.Surface { GroupID = 1, Name = "Material", - Color = new Color(231, 76, 60), + Color = new Color(181, 89, 49), Archetypes = Archetypes.Material.Nodes }, new GroupArchetype { GroupID = 2, Name = "Constants", - Color = new Color(243, 156, 18), + Color = new Color(163, 106, 21), Archetypes = Archetypes.Constants.Nodes }, new GroupArchetype { GroupID = 3, Name = "Math", - Color = new Color(52, 152, 219), + Color = new Color(45, 126, 181), Archetypes = Archetypes.Math.Nodes }, new GroupArchetype { GroupID = 4, Name = "Packing", - Color = new Color(155, 89, 182), + Color = new Color(124, 66, 143), Archetypes = Archetypes.Packing.Nodes }, new GroupArchetype { GroupID = 5, Name = "Textures", - Color = new Color(46, 204, 113), + Color = new Color(43, 130, 83), Archetypes = Archetypes.Textures.Nodes }, new GroupArchetype { GroupID = 6, Name = "Parameters", - Color = new Color(52, 73, 94), + Color = new Color(55, 78, 99), Archetypes = Archetypes.Parameters.Nodes }, new GroupArchetype { GroupID = 7, Name = "Tools", - Color = new Color(149, 165, 166), + Color = new Color(88, 96, 97), Archetypes = Archetypes.Tools.Nodes }, new GroupArchetype { GroupID = 8, Name = "Layers", - Color = new Color(249, 105, 116), + Color = new Color(189, 75, 81), Archetypes = Archetypes.Layers.Nodes }, new GroupArchetype @@ -123,21 +123,21 @@ namespace FlaxEditor.Surface { GroupID = 10, Name = "Boolean", - Color = new Color(237, 28, 36), + Color = new Color(166, 27, 32), Archetypes = Archetypes.Boolean.Nodes }, new GroupArchetype { GroupID = 11, Name = "Bitwise", - Color = new Color(181, 230, 29), + Color = new Color(96, 125, 34), Archetypes = Archetypes.Bitwise.Nodes }, new GroupArchetype { GroupID = 12, Name = "Comparisons", - Color = new Color(148, 30, 34), + Color = new Color(166, 33, 57), Archetypes = Archetypes.Comparisons.Nodes }, // GroupID = 13 -> Custom Nodes provided externally diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 6312bd68d..4b918de08 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -55,6 +55,11 @@ namespace FlaxEditor.Surface /// protected Rectangle _headerRect; + /// + /// The header text rectangle (local space). + /// + protected Rectangle _headerTextRect; + /// /// The close button rectangle (local space). /// @@ -132,7 +137,7 @@ namespace FlaxEditor.Surface AutoFocus = false; TooltipText = GetTooltip(); CullChildren = false; - BackgroundColor = Style.Current.BackgroundNormal; + BackgroundColor = Color.Lerp(Style.Current.Background, Style.Current.BackgroundHighlighted, 0.55f); if (Archetype.DefaultValues != null) { @@ -149,7 +154,7 @@ namespace FlaxEditor.Surface /// /// Gets the color of the footer of the node. /// - protected virtual Color FooterColor => GroupArchetype.Color; + protected virtual Color ArchetypeColor => GroupArchetype.Color; private Float2 mouseDownMousePosition; @@ -1032,6 +1037,7 @@ namespace FlaxEditor.Surface const float closeButtonMargin = Constants.NodeCloseButtonMargin; const float closeButtonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); + _headerTextRect = _headerRect with { Width = _headerRect.Width - 5f, X = _headerRect.X + 5f }; _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); _footerRect = new Rectangle(0, Height - footerSize, Width, footerSize); } @@ -1058,8 +1064,8 @@ namespace FlaxEditor.Surface var headerColor = style.BackgroundHighlighted; if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting) headerColor *= 1.07f; - Render2D.FillRectangle(_headerRect, headerColor); - Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center); + Render2D.FillRectangle(_headerRect, ArchetypeColor); + Render2D.DrawText(style.FontLarge, Title, _headerTextRect, style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, Constants.NodeHeaderTextScale); // Close button if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) @@ -1069,7 +1075,7 @@ namespace FlaxEditor.Surface } // Footer - Render2D.FillRectangle(_footerRect, FooterColor); + Render2D.FillRectangle(_footerRect, ArchetypeColor); DrawChildren(); diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index be09f51be..654cf7abf 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -140,6 +140,11 @@ namespace FlaxEditor.Surface /// public Texture Background; + /// + /// The color used as a surface background. + /// + public Color BackgroundColor; + /// /// Boxes drawing callback. /// @@ -216,7 +221,7 @@ namespace FlaxEditor.Surface private static void DefaultDrawBox(Elements.Box box) { - var rect = new Rectangle(Float2.Zero, box.Size); + var rect = new Rectangle(0.0f, box.Height * 0.5f - Constants.BoxSize * 0.5f, new Float2(Constants.BoxSize)); // Size culling const float minBoxSize = 5.0f; @@ -293,6 +298,7 @@ namespace FlaxEditor.Surface ArrowClose = editor.Icons.VisjectArrowClosed32, }, Background = editor.UI.VisjectSurfaceBackground, + BackgroundColor = new Color(31, 31, 31), }; } diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index af5893907..0138bce8d 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -65,7 +65,7 @@ namespace FlaxEditor.Surface /// protected virtual void DrawBackground() { - DrawBackgroundDefault(Style.Background, Width, Height); + DrawBackgroundSolidColor(Style.BackgroundColor, Width, Height); } internal static void DrawBackgroundDefault(Texture background, float width, float height) @@ -95,6 +95,12 @@ namespace FlaxEditor.Surface } } + internal static void DrawBackgroundSolidColor(Color color, float width, float height) + { + Rectangle backgroundRect = new Rectangle(0f, 0f, width, height); + Render2D.FillRectangle(backgroundRect, color); + } + /// /// Draws the selection background. /// From e27287080366984ad2534f380dfdecf1bc6a44ef Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 18 Dec 2025 19:51:17 +0100 Subject: [PATCH 04/87] increase max zoom --- Source/Editor/Surface/VisjectSurface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index e3bb94bcc..dfc8ea7a2 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -217,7 +217,7 @@ namespace FlaxEditor.Surface set { // Clamp - value = Mathf.Clamp(value, 0.05f, 1.6f); + value = Mathf.Clamp(value, 0.05f, 1.85f); // Check if value will change if (Mathf.Abs(value - _targetScale) > 0.0001f) From d3d67fddcc807308b79076fc4b446253d2728160 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 18 Dec 2025 19:54:31 +0100 Subject: [PATCH 05/87] add node shadow - rectangular shadow (can be disabled...) - ... for example for nodes with more complex shapes like reroute --- Source/Editor/Surface/Archetypes/Tools.cs | 6 ++++++ Source/Editor/Surface/SurfaceNode.cs | 18 +++++++++++++++++- Source/Engine/Core/Math/Rectangle.cs | 10 ++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 3d0a8cd0c..555f7ad55 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1059,6 +1059,7 @@ namespace FlaxEditor.Surface.Archetypes internal class RerouteNode : SurfaceNode, IConnectionInstigator { internal static readonly Float2 DefaultSize = new Float2(FlaxEditor.Surface.Constants.BoxRowHeight); + internal override bool DrawBasicShadow => false; private Rectangle _localBounds; private InputBox _input; private OutputBox _output; @@ -1187,6 +1188,11 @@ namespace FlaxEditor.Surface.Archetypes icon = type.IsVoid ? style.Icons.ArrowClose : style.Icons.BoxClose; else icon = type.IsVoid ? style.Icons.ArrowOpen : style.Icons.BoxOpen; + + // Shadow + var shadowRect = _localBounds.MakeOffsetted(ShadowOffset); + Render2D.DrawSprite(icon, shadowRect, Color.Black.AlphaMultiplied(0.125f)); + Render2D.DrawSprite(icon, _localBounds, connectionColor); base.Draw(); diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 4b918de08..89471eaf4 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -40,6 +40,13 @@ namespace FlaxEditor.Surface [HideInEditor] public class SurfaceNode : SurfaceControl { + internal const float ShadowOffset = 2.25f; + + /// + /// If true, draws a basic rectangle shadow behind the node. Disable to hide shadow or if the node is drawing a custom shadow. + /// + internal virtual bool DrawBasicShadow => true; + /// /// The box to draw a highlight around. Drawing will be skipped if null. /// @@ -1047,8 +1054,16 @@ namespace FlaxEditor.Surface { var style = Style.Current; - // Background var backgroundRect = new Rectangle(Float2.Zero, Size); + + // Shadow + if (DrawBasicShadow) + { + var shadowRect = backgroundRect.MakeOffsetted(ShadowOffset); + Render2D.FillRectangle(shadowRect, Color.Black.AlphaMultiplied(0.125f)); + } + + // Background Render2D.FillRectangle(backgroundRect, BackgroundColor); // Breakpoint hit @@ -1085,6 +1100,7 @@ namespace FlaxEditor.Surface var colorTop = Color.Orange; var colorBottom = Color.OrangeRed; Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom); + Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom, 2.5f); } // Breakpoint dot diff --git a/Source/Engine/Core/Math/Rectangle.cs b/Source/Engine/Core/Math/Rectangle.cs index 81c689d48..c2339d9d5 100644 --- a/Source/Engine/Core/Math/Rectangle.cs +++ b/Source/Engine/Core/Math/Rectangle.cs @@ -253,6 +253,16 @@ namespace FlaxEngine return new Rectangle(Location + new Float2(x, y), Size); } + /// + /// Make offseted rectangle + /// + /// Offset (will be applied to X- and Y- axis). + /// Offseted rectangle. + public Rectangle MakeOffsetted(float offset) + { + return new Rectangle(Location + new Float2(offset, offset), Size); + } + /// /// Make offseted rectangle /// From 31b1ceb9f03a3cd2e32e32a46624e1384efa418a Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 18 Dec 2025 19:57:58 +0100 Subject: [PATCH 06/87] increase selection outline width --- Source/Editor/Surface/SurfaceNode.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 89471eaf4..ad74f58ae 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -1099,7 +1099,6 @@ namespace FlaxEditor.Surface { var colorTop = Color.Orange; var colorBottom = Color.OrangeRed; - Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom); Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom, 2.5f); } From cfda18ea9d7b7074a63988bf958d18b846ee0eef Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 18 Dec 2025 22:41:55 +0100 Subject: [PATCH 07/87] propagate disabled node boxes across reroute nodes --- Source/Editor/Surface/Archetypes/Tools.cs | 10 ++++++++++ Source/Editor/Surface/Elements/Box.cs | 11 +++++++++++ Source/Editor/Surface/Elements/OutputBox.cs | 4 ++-- Source/Editor/Surface/SurfaceStyle.cs | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 555f7ad55..685068986 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1059,7 +1059,10 @@ namespace FlaxEditor.Surface.Archetypes internal class RerouteNode : SurfaceNode, IConnectionInstigator { internal static readonly Float2 DefaultSize = new Float2(FlaxEditor.Surface.Constants.BoxRowHeight); + + internal bool DrawDisabled => _input.AllConnectionsDisabled || _output.AllConnectionsDisabled; internal override bool DrawBasicShadow => false; + private Rectangle _localBounds; private InputBox _input; private OutputBox _output; @@ -1165,6 +1168,9 @@ namespace FlaxEditor.Surface.Archetypes /// public override void Draw() { + // Update active state of input + _input.IsActive = !_output.AllConnectionsDisabled; + var style = Surface.Style; var connectionColor = style.Colors.Default; var type = ScriptType.Null; @@ -1178,6 +1184,10 @@ namespace FlaxEditor.Surface.Archetypes Surface.Style.GetConnectionColor(type, hints, out connectionColor); } + // Draw the box as disabled if needed + if (DrawDisabled) + connectionColor = connectionColor * 0.6f; + if (!_input.HasAnyConnection) Render2D.FillRectangle(new Rectangle(-barHorizontalOffset - barHeight * 2, (DefaultSize.Y - barHeight) / 2, barHeight * 2, barHeight), connectionColor); if (!_output.HasAnyConnection) diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index 08023ac93..4c86af041 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FlaxEditor.Scripting; using FlaxEditor.Surface.Undo; using FlaxEngine; @@ -194,6 +195,16 @@ namespace FlaxEditor.Surface.Elements set => _isActive = value; } + /// + /// Gets if the box is disabled (user can still connect, but connections will be ignored). + /// + public bool IsDisabled => !(Enabled && IsActive); + + /// + /// Gets a value indicating whether all connections are disabled. + /// + public bool AllConnectionsDisabled => Connections.All(c => c.IsDisabled); + /// protected Box(SurfaceNode parentNode, NodeElementArchetype archetype, Float2 location) : base(parentNode, archetype, location, new Float2(Constants.BoxRowHeight), false) diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index 7e271fef0..0673f694b 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -190,7 +190,7 @@ namespace FlaxEditor.Surface.Elements Box targetBox = Connections[i]; var endPos = targetBox.ConnectionOrigin; var highlight = DefaultConnectionThickness + Mathf.Max(startHighlight, targetBox.ConnectionsHighlightIntensity); - var alpha = targetBox.Enabled && targetBox.IsActive ? 1.0f : 0.6f; + var alpha = targetBox.IsDisabled ? 0.6f : 1.0f; // We have to calculate an offset here to preserve the original color for when the default connection thickness is larger than 1 var highlightOffset = (highlight - (DefaultConnectionThickness - 1)); @@ -216,7 +216,7 @@ namespace FlaxEditor.Surface.Elements // Draw all the connections var startPos = ConnectionOrigin; var endPos = targetBox.ConnectionOrigin; - var alpha = targetBox.Enabled && targetBox.IsActive ? 1.0f : 0.6f; + var alpha = targetBox.IsDisabled ? 0.6f : 1.0f; var color = _currentTypeColor * alpha; DrawConnection(Surface.Style, ref startPos, ref endPos, ref color, SelectedConnectionThickness); } diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index 654cf7abf..86707efd3 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -233,7 +233,7 @@ namespace FlaxEditor.Surface // Draw icon bool hasConnections = box.HasAnyConnection; - float alpha = box.Enabled && box.IsActive ? 1.0f : 0.6f; + float alpha = box.IsDisabled ? 0.6f : 1.0f; Color color = box.CurrentTypeColor * alpha; var style = box.Surface.Style; SpriteHandle icon; From 21d46dad070d02af73f2b470ebc93237537f7ec2 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 18 Dec 2025 22:55:56 +0100 Subject: [PATCH 08/87] change how connected boxes are drawn --- Source/Editor/Surface/Constants.cs | 2 +- Source/Editor/Surface/SurfaceStyle.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 3310351bf..80aecd3ec 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -55,7 +55,7 @@ namespace FlaxEditor.Surface /// /// The box size (with and height). /// - public const float BoxSize = 12.0f; + public const float BoxSize = 13.0f; /// /// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent. diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index 86707efd3..8d547c14e 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.Scripting; +using FlaxEditor.Surface.Elements; using FlaxEngine; using FlaxEngine.Utilities; @@ -228,8 +229,9 @@ namespace FlaxEditor.Surface if (rect.Size.LengthSquared < minBoxSize * minBoxSize) return; - // Debugging boxes size + // Debugging boxes size and bounds //Render2D.DrawRectangle(rect, Color.Orange); return; + //Render2D.DrawRectangle(box.Bounds, Color.Green); // Draw icon bool hasConnections = box.HasAnyConnection; @@ -244,13 +246,23 @@ namespace FlaxEditor.Surface color *= box.ConnectionsHighlightIntensity + 1; Render2D.DrawSprite(icon, rect, color); + // Draw connected hint with color from connected output box + if (hasConnections && box.Connections[0] is OutputBox connectedOutputBox) + { + bool connectedSameColor = connectedOutputBox.CurrentTypeColor == box.CurrentTypeColor; + Color innerColor = connectedSameColor ? color.RGBMultiplied(0.4f) : connectedOutputBox.CurrentTypeColor; + innerColor = innerColor * alpha; + Render2D.DrawSprite(icon, rect.MakeExpanded(-5.0f), innerColor); + } + // Draw selection hint if (box.IsSelected) { float outlineAlpha = Mathf.Sin(Time.TimeSinceStartup * 4.0f) * 0.5f + 0.5f; float outlineWidth = Mathf.Lerp(1.5f, 4.0f, outlineAlpha); var outlineRect = new Rectangle(rect.X - outlineWidth, rect.Y - outlineWidth, rect.Width + outlineWidth * 2, rect.Height + outlineWidth * 2); - Render2D.DrawSprite(icon, outlineRect, FlaxEngine.GUI.Style.Current.BorderSelected.RGBMultiplied(1.0f + outlineAlpha * 0.4f)); + Color selectionColor = FlaxEngine.GUI.Style.Current.BorderSelected.RGBMultiplied(1.0f + outlineAlpha * 0.4f); + Render2D.DrawSprite(icon, outlineRect, selectionColor.AlphaMultiplied(0.4f)); } } From a98a76f6e5d2bce1a5b6390f882ad35ca665e1ec Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 20 Dec 2025 00:02:11 +0100 Subject: [PATCH 09/87] fix box spacing and (auto calculate) node height - Work in progress but enough progress to commit because it basically works, it's just a bit ugly - Node height is now recalculated every time a new element is added to the node - Introduced `Archetype.UseFixedSize` for special nodes like *Color Gradient* or *Curve* that must rely on hardcoded size for now because the auto sizing does not take elements like the gradient editor or curve editor into account (ideally in the future it will) - Fixed input and output box spacing (still some 1px offsets that I'm unsure where they are from, could be placebo though) --- .../Archetypes/Animation.StateMachine.cs | 2 +- Source/Editor/Surface/Archetypes/Animation.cs | 4 +-- .../Editor/Surface/Archetypes/BehaviorTree.cs | 6 ++--- Source/Editor/Surface/Archetypes/Constants.cs | 6 ++--- Source/Editor/Surface/Archetypes/Material.cs | 2 +- .../Surface/Archetypes/ParticleModules.cs | 2 +- Source/Editor/Surface/Archetypes/Particles.cs | 4 +-- Source/Editor/Surface/Archetypes/Textures.cs | 2 +- Source/Editor/Surface/Archetypes/Tools.cs | 11 +++++--- Source/Editor/Surface/Constants.cs | 16 +++++++++--- Source/Editor/Surface/Elements/InputBox.cs | 2 +- Source/Editor/Surface/Elements/OutputBox.cs | 2 +- Source/Editor/Surface/NodeArchetype.cs | 2 ++ Source/Editor/Surface/NodeElementArchetype.cs | 26 +++++++++---------- .../Editor/Surface/ParticleEmitterSurface.cs | 4 +-- Source/Editor/Surface/SurfaceComment.cs | 4 +-- Source/Editor/Surface/SurfaceNode.cs | 21 ++++++++++----- 17 files changed, 69 insertions(+), 47 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index e66f38398..b4afff091 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -79,7 +79,7 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch) { var marginX = FlaxEditor.Surface.Constants.NodeMarginX; - var uiStartPosY = FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize; + var uiStartPosY = FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight; var editButton = new Button(marginX, uiStartPosY, 246, 20) { diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index 9f3112905..ad5e45611 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -244,7 +244,7 @@ namespace FlaxEditor.Surface.Archetypes Type = NodeElementType.Input, Position = new Float2( FlaxEditor.Surface.Constants.NodeMarginX - FlaxEditor.Surface.Constants.BoxOffsetX, - FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + ylevel * FlaxEditor.Surface.Constants.LayoutOffsetY), + FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + ylevel * FlaxEditor.Surface.Constants.LayoutOffsetY), Text = "Pose " + _blendPoses.Count, Single = true, ValueIndex = -1, @@ -263,7 +263,7 @@ namespace FlaxEditor.Surface.Archetypes private void UpdateHeight() { float nodeHeight = 10 + (Mathf.Max(_blendPoses.Count, 1) + 3) * FlaxEditor.Surface.Constants.LayoutOffsetY; - Height = nodeHeight + FlaxEditor.Surface.Constants.NodeMarginY * 2 + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.NodeFooterSize; + Height = nodeHeight + FlaxEditor.Surface.Constants.NodeMarginY * 2 + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.NodeFooterSize; } /// diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 9c9325177..63f6f5c2d 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -515,7 +515,7 @@ namespace FlaxEditor.Surface.Archetypes height += decorator.Height + DecoratorsMarginY; width = Mathf.Max(width, decorator.Width - FlaxEditor.Surface.Constants.NodeCloseButtonSize - 2 * DecoratorsMarginX); } - Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.NodeFooterSize); + Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.NodeFooterSize); UpdateRectangles(); } @@ -537,7 +537,7 @@ namespace FlaxEditor.Surface.Archetypes decorator.IndexInParent = indexInParent + 1; // Push elements above the node } const float footerSize = FlaxEditor.Surface.Constants.NodeFooterSize; - const float headerSize = FlaxEditor.Surface.Constants.NodeHeaderSize; + const float headerSize = FlaxEditor.Surface.Constants.NodeHeaderHeight; const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerSize); @@ -676,7 +676,7 @@ namespace FlaxEditor.Surface.Archetypes width = Mathf.Max(width, _debugInfoSize.X + 8.0f); height += _debugInfoSize.Y + 8.0f; } - return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2 + DecoratorsMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderSize); + return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2 + DecoratorsMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderHeight); } protected override void UpdateRectangles() diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index ad88fa56a..680cc512d 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new EnumComboBox(type) { EnumTypeValue = Values[0], - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.EnumTypeValue); @@ -218,7 +218,7 @@ namespace FlaxEditor.Surface.Archetypes _output = (OutputBox)Elements[0]; _typePicker = new TypePickerControl { - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _typePicker.ValueChanged += () => Set(3); @@ -362,7 +362,7 @@ namespace FlaxEditor.Surface.Archetypes _output = (OutputBox)Elements[0]; _keyTypePicker = new TypePickerControl { - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _keyTypePicker.ValueChanged += OnKeyTypeChanged; diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index e46038639..f707e4c2b 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -323,7 +323,7 @@ namespace FlaxEditor.Surface.Archetypes public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { - Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size; + Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; if (nodeArch.TypeID == 8) { pos += new Float2(60, 0); diff --git a/Source/Editor/Surface/Archetypes/ParticleModules.cs b/Source/Editor/Surface/Archetypes/ParticleModules.cs index 4a68c31e0..debc88d59 100644 --- a/Source/Editor/Surface/Archetypes/ParticleModules.cs +++ b/Source/Editor/Surface/Archetypes/ParticleModules.cs @@ -461,7 +461,7 @@ namespace FlaxEditor.Surface.Archetypes /// /// The particle module node elements offset applied to controls to reduce default surface node header thickness. /// - private const float NodeElementsOffset = 16.0f - Surface.Constants.NodeHeaderSize; + private const float NodeElementsOffset = 16.0f - Surface.Constants.NodeHeaderHeight; private const NodeFlags DefaultModuleFlags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoMove; diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs index 5b8c05381..d5319b875 100644 --- a/Source/Editor/Surface/Archetypes/Particles.cs +++ b/Source/Editor/Surface/Archetypes/Particles.cs @@ -74,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes /// /// The header height. /// - public const float HeaderHeight = FlaxEditor.Surface.Constants.NodeHeaderSize; + public const float HeaderHeight = FlaxEditor.Surface.Constants.NodeHeaderHeight; /// /// Gets the type of the module. @@ -199,7 +199,7 @@ namespace FlaxEditor.Surface.Archetypes DrawChildren(); // Options border - var optionsAreaStart = FlaxEditor.Surface.Constants.NodeHeaderSize + 3.0f; + var optionsAreaStart = FlaxEditor.Surface.Constants.NodeHeaderHeight + 3.0f; var optionsAreaHeight = 7 * FlaxEditor.Surface.Constants.LayoutOffsetY + 6.0f; Render2D.DrawRectangle(new Rectangle(1, optionsAreaStart, Width - 2, optionsAreaHeight), style.BackgroundSelected); diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index c1132e947..4a5bff299 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -54,7 +54,7 @@ namespace FlaxEditor.Surface.Archetypes { _textureGroupPicker = new ComboBox { - Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * 5), + Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.LayoutOffsetY * 5), Width = 100, Parent = this, }; diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 685068986..13691a05b 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -467,6 +467,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new CurveNode(id, context, arch, groupArch), Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.", Flags = NodeFlags.AllGraphs, + UseFixedSize = true, Size = new Float2(400, 180.0f), DefaultValues = new object[] { @@ -828,7 +829,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = ScriptType.FlaxObject, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -897,7 +898,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = ScriptType.Object, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 140, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 140, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -948,7 +949,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = ScriptType.FlaxObject, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -999,7 +1000,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = type, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -1510,6 +1511,7 @@ namespace FlaxEditor.Surface.Archetypes Description = "Linear color gradient sampler", Flags = NodeFlags.AllGraphs, Size = new Float2(400, 150.0f), + UseFixedSize = true, DefaultValues = new object[] { // Stops count @@ -1829,6 +1831,7 @@ namespace FlaxEditor.Surface.Archetypes Description = "Reroute a connection.", Flags = NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaGUI | NodeFlags.AllGraphs, Size = RerouteNode.DefaultSize, + UseFixedSize = true, ConnectionsHints = ConnectionsHint.All, IndependentBoxes = new int[] { 0 }, DependentBoxes = new int[] { 1 }, diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 80aecd3ec..58dee6084 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -23,7 +23,7 @@ namespace FlaxEditor.Surface /// /// The node header height. /// - public const float NodeHeaderSize = 20.0f; + public const float NodeHeaderHeight = 20.0f; public const float NodeHeaderTextScale = 0.65f; @@ -35,7 +35,7 @@ namespace FlaxEditor.Surface /// /// The node left margin. /// - public const float NodeMarginX = 5.0f; + public const float NodeMarginX = 8.0f; /// /// The node right margin. @@ -45,7 +45,7 @@ namespace FlaxEditor.Surface /// /// The box position offset on the x axis. /// - public const float BoxOffsetX = 2.0f; + public const float BoxOffsetX = 0.0f; /// /// The width of the row that is started by a box. @@ -61,5 +61,15 @@ namespace FlaxEditor.Surface /// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent. /// public const float LayoutOffsetY = 22.0f; + + /// + /// The offset between the box text and the box + /// + public const float BoxTextOffset = 4.0f; + + /// + /// The width of the rectangle used to draw the box text. + /// + public const float BoxTextRectWidth = 1410.0f; } } diff --git a/Source/Editor/Surface/Elements/InputBox.cs b/Source/Editor/Surface/Elements/InputBox.cs index 9861ebacd..113da6c68 100644 --- a/Source/Editor/Surface/Elements/InputBox.cs +++ b/Source/Editor/Surface/Elements/InputBox.cs @@ -1442,7 +1442,7 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; - var rect = new Rectangle(Width + 4, 0, 1410, Height); + var rect = new Rectangle(Width + Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); Render2D.DrawText(style.FontSmall, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); } diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index 0673f694b..025b16f4b 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -234,7 +234,7 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; - var rect = new Rectangle(-100, 0, 100 - 2, Height); + var rect = new Rectangle(-Constants.BoxTextRectWidth - Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); Render2D.DrawText(style.FontSmall, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center); } } diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index b29dd0956..18e6ac8c9 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -129,6 +129,8 @@ namespace FlaxEditor.Surface /// public NodeFlags Flags; + public bool UseFixedSize = false; + /// /// Title text. /// diff --git a/Source/Editor/Surface/NodeElementArchetype.cs b/Source/Editor/Surface/NodeElementArchetype.cs index 1a9d4ae74..d8008f53f 100644 --- a/Source/Editor/Surface/NodeElementArchetype.cs +++ b/Source/Editor/Surface/NodeElementArchetype.cs @@ -78,12 +78,12 @@ namespace FlaxEditor.Surface /// /// Gets the actual element position on the y axis. /// - public float ActualPositionY => Position.Y + Constants.NodeMarginY + Constants.NodeHeaderSize; + public float ActualPositionY => Position.Y + Constants.NodeMarginY + Constants.NodeHeaderHeight; /// /// Gets the actual element position. /// - public Float2 ActualPosition => new Float2(Position.X + Constants.NodeMarginX, Position.Y + Constants.NodeMarginY + Constants.NodeHeaderSize); + public Float2 ActualPosition => new Float2(Position.X + Constants.NodeMarginX, Position.Y + Constants.NodeMarginY + Constants.NodeHeaderHeight); /// /// Node element archetypes factory object. Helps to build surface nodes archetypes. @@ -107,7 +107,7 @@ namespace FlaxEditor.Surface Type = NodeElementType.Input, Position = new Float2( Constants.NodeMarginX - Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = valueIndex, @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface Type = NodeElementType.Input, Position = new Float2( Constants.NodeMarginX - Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = valueIndex, @@ -158,7 +158,7 @@ namespace FlaxEditor.Surface Type = NodeElementType.Output, Position = new Float2( Constants.NodeMarginX - Constants.BoxRowHeight + Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = -1, @@ -182,8 +182,8 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxRowHeight + Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + Constants.NodeMarginX - Constants.BoxSize + Constants.BoxOffsetX, + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = -1, @@ -228,7 +228,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.IntegerValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Text = null, Single = false, ValueIndex = valueIndex, @@ -254,7 +254,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.UnsignedIntegerValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Text = null, Single = false, ValueIndex = valueIndex, @@ -280,7 +280,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.FloatValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Text = null, Single = false, ValueIndex = valueIndex, @@ -359,7 +359,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.ColorValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Text = null, Single = false, ValueIndex = valueIndex, @@ -530,7 +530,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.TextBox, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Size = new Float2(width, height), Single = false, ValueIndex = valueIndex, @@ -595,7 +595,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.BoxValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Text = null, Single = false, ValueIndex = valueIndex, diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs index f332471fb..c61d4365d 100644 --- a/Source/Editor/Surface/ParticleEmitterSurface.cs +++ b/Source/Editor/Surface/ParticleEmitterSurface.cs @@ -59,7 +59,7 @@ namespace FlaxEditor.Surface var width = _rootNode.Width; var rootPos = _rootNode.Location; var pos = rootPos; - pos.Y += Constants.NodeHeaderSize + 1.0f + 7 * Constants.LayoutOffsetY + 6.0f + 4.0f; + pos.Y += Constants.NodeHeaderHeight + 1.0f + 7 * Constants.LayoutOffsetY + 6.0f + 4.0f; for (int i = 0; i < _rootNode.Headers.Length; i++) { @@ -67,7 +67,7 @@ namespace FlaxEditor.Surface var modulesStart = pos - rootPos; var modules = modulesGroups.FirstOrDefault(x => x.Key == header.ModuleType); - pos.Y += Constants.NodeHeaderSize + 2.0f; + pos.Y += Constants.NodeHeaderHeight + 2.0f; if (modules != null) { foreach (var module in modules) diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 10e9fc776..3c04cb736 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -70,7 +70,7 @@ namespace FlaxEditor.Surface { _renameTextBox = new TextBox(false, 0, 0, Width) { - Height = Constants.NodeHeaderSize, + Height = Constants.NodeHeaderHeight, Visible = false, Parent = this, EndEditOnClick = false, // We have to handle this ourselves, otherwise the textbox instantly loses focus when double-clicking the header @@ -158,7 +158,7 @@ namespace FlaxEditor.Surface /// protected override void UpdateRectangles() { - const float headerSize = Constants.NodeHeaderSize; + const float headerSize = Constants.NodeHeaderHeight; const float buttonMargin = Constants.NodeCloseButtonMargin; const float buttonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index ad74f58ae..56c327f49 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -135,7 +135,7 @@ namespace FlaxEditor.Surface /// The node archetype. /// The group archetype. public SurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) - : base(context, nodeArch.Size.X + Constants.NodeMarginX * 2, nodeArch.Size.Y + Constants.NodeMarginY * 2 + Constants.NodeHeaderSize + Constants.NodeFooterSize) + : base(context, nodeArch.Size.X + Constants.NodeMarginX * 2, nodeArch.Size.Y + Constants.NodeMarginY * 2 + Constants.NodeHeaderHeight + Constants.NodeFooterSize) { Title = nodeArch.Title; ID = id; @@ -173,7 +173,7 @@ namespace FlaxEditor.Surface /// The node control total size. protected virtual Float2 CalculateNodeSize(float width, float height) { - return new Float2(width + Constants.NodeMarginX * 2, height + Constants.NodeMarginY * 2 + Constants.NodeHeaderSize + Constants.NodeFooterSize); + return new Float2(width + Constants.NodeMarginX * 2, height + Constants.NodeMarginY * 2 + Constants.NodeHeaderHeight + Constants.NodeFooterSize); } /// @@ -227,25 +227,28 @@ namespace FlaxEditor.Surface var child = Children[i]; if (!child.Visible) continue; + // Input boxes if (child is InputBox inputBox) { var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 20; if (inputBox.DefaultValueEditor != null) boxWidth += inputBox.DefaultValueEditor.Width + 4; leftWidth = Mathf.Max(leftWidth, boxWidth); - leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f); + leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + 20.0f); } + // Output boxes else if (child is OutputBox outputBox) { rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 20); - rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f); + rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + 20.0f); } + // Other controls in the node else if (child is Control control) { if (control.AnchorPreset == AnchorPresets.TopLeft) { width = Mathf.Max(width, control.Right + 4 - Constants.NodeMarginX); - height = Mathf.Max(height, control.Bottom + 4 - Constants.NodeMarginY - Constants.NodeHeaderSize); + height = Mathf.Max(height, control.Bottom + 4 - Constants.NodeMarginY - Constants.NodeHeaderHeight); } else if (!_headerRect.Intersects(control.Bounds)) { @@ -337,6 +340,10 @@ namespace FlaxEditor.Surface Elements.Add(element); if (element is Control control) AddChild(control); + + // TODO: Perform this at a better time instead of every time an element gets added. + if (!Archetype.UseFixedSize) + ResizeAuto(); } /// @@ -377,7 +384,7 @@ namespace FlaxEditor.Surface // Sync properties for exiting box box.Text = text; box.CurrentType = type; - box.Y = Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY; + box.Y = Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY; } // Update box @@ -1040,7 +1047,7 @@ namespace FlaxEditor.Surface protected override void UpdateRectangles() { const float footerSize = Constants.NodeFooterSize; - const float headerSize = Constants.NodeHeaderSize; + const float headerSize = Constants.NodeHeaderHeight; const float closeButtonMargin = Constants.NodeCloseButtonMargin; const float closeButtonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); From a92d2b6ca2353d862c60086e77507a2e0a0955a7 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 20 Dec 2025 16:58:35 +0100 Subject: [PATCH 10/87] add slight background grid --- Source/Editor/Surface/VisjectSurface.Draw.cs | 47 +++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index 0138bce8d..0b7f6eaae 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -66,6 +66,47 @@ namespace FlaxEditor.Surface protected virtual void DrawBackground() { DrawBackgroundSolidColor(Style.BackgroundColor, Width, Height); + DrawGridBackground(Width, Height); + } + + // TODO: Rename (and get rid of old texture based draw background?) + internal static void DrawBackgroundSolidColor(Color color, float width, float height) + { + Rectangle backgroundRect = new Rectangle(0f, 0f, width, height); + Render2D.FillRectangle(backgroundRect, color); + } + + internal void DrawGridBackground(float width, float height) + { + var viewRect = GetClientArea(); + var upperLeft = _rootControl.PointFromParent(viewRect.Location); + var bottomRight = _rootControl.PointFromParent(viewRect.Size); + var min = Float2.Min(upperLeft, bottomRight); + var max = Float2.Max(upperLeft, bottomRight); + var pixelRange = (max - min) * ViewScale * 2.75f; + Render2D.PushClip(ref viewRect); + DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X); + DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y); + Render2D.PopClip(); + } + + private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange) + { + var linesColor = Style.BackgroundColor.RGBMultiplied(1.2f); + float[] _gridTickStrengths = { 0f }; + Utilities.Utils.DrawCurveTicks((decimal tick, double step, float strength) => + { + var p = _rootControl.PointToParent(axis * (float)tick); ; + + // Draw line + var lineRect = new Rectangle + ( + viewRect.Location + (p - 0.5f) * axis, + Float2.Lerp(viewRect.Size, Float2.One, axis) + ); + Render2D.FillRectangle(lineRect, linesColor.AlphaMultiplied(strength)); + + }, Utilities.Utils.CurveTickSteps, ref _gridTickStrengths, min, max, pixelRange); } internal static void DrawBackgroundDefault(Texture background, float width, float height) @@ -95,12 +136,6 @@ namespace FlaxEditor.Surface } } - internal static void DrawBackgroundSolidColor(Color color, float width, float height) - { - Rectangle backgroundRect = new Rectangle(0f, 0f, width, height); - Render2D.FillRectangle(backgroundRect, color); - } - /// /// Draws the selection background. /// From 3279016067f4ef1ead1c6f2fb8b3c04b25cd92f4 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 20 Dec 2025 20:32:40 +0100 Subject: [PATCH 11/87] tweak some node spacing --- Source/Editor/Surface/Constants.cs | 22 ++++++++++++++------- Source/Editor/Surface/Elements/InputBox.cs | 2 +- Source/Editor/Surface/Elements/OutputBox.cs | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 58dee6084..bec0b5183 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -13,7 +13,7 @@ namespace FlaxEditor.Surface /// /// The node close button size. /// - public const float NodeCloseButtonSize = 12.0f; + public const float NodeCloseButtonSize = 16.0f; /// /// The node close button margin from the edges. @@ -25,6 +25,9 @@ namespace FlaxEditor.Surface /// public const float NodeHeaderHeight = 20.0f; + /// + /// The scale of the header text. + /// public const float NodeHeaderTextScale = 0.65f; /// @@ -33,14 +36,14 @@ namespace FlaxEditor.Surface public const float NodeFooterSize = 2.0f; /// - /// The node left margin. + /// The horizontal node margin. /// public const float NodeMarginX = 8.0f; /// - /// The node right margin. + /// The vertical node right margin. /// - public const float NodeMarginY = 5.0f; + public const float NodeMarginY = 7.0f; /// /// The box position offset on the x axis. @@ -50,7 +53,7 @@ namespace FlaxEditor.Surface /// /// The width of the row that is started by a box. /// - public const float BoxRowHeight = 18.0f; + public const float BoxRowHeight = 19.0f; /// /// The box size (with and height). @@ -60,7 +63,7 @@ namespace FlaxEditor.Surface /// /// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent. /// - public const float LayoutOffsetY = 22.0f; + public const float LayoutOffsetY = 24.0f; /// /// The offset between the box text and the box @@ -70,6 +73,11 @@ namespace FlaxEditor.Surface /// /// The width of the rectangle used to draw the box text. /// - public const float BoxTextRectWidth = 1410.0f; + public const float BoxTextRectWidth = 500.0f; + + /// + /// The scale of text of boxes. + /// + public const float BoxTextScale = 1.175f; } } diff --git a/Source/Editor/Surface/Elements/InputBox.cs b/Source/Editor/Surface/Elements/InputBox.cs index 113da6c68..5a4bfc31c 100644 --- a/Source/Editor/Surface/Elements/InputBox.cs +++ b/Source/Editor/Surface/Elements/InputBox.cs @@ -1443,7 +1443,7 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; var rect = new Rectangle(Width + Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); - Render2D.DrawText(style.FontSmall, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, Constants.BoxTextScale); } /// diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index 025b16f4b..3e473d7ac 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -235,7 +235,7 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; var rect = new Rectangle(-Constants.BoxTextRectWidth - Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); - Render2D.DrawText(style.FontSmall, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center, TextWrapping.NoWrap, 1f, Constants.BoxTextScale); } } } From 24a11ac2a8a7c8dddc56e2f912ee633403b45000 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 28 Feb 2026 12:38:07 -0600 Subject: [PATCH 12/87] Add tree view mode for content window. --- .../Editor/Content/GUI/ContentNavigation.cs | 8 +- Source/Editor/Content/Items/ContentFolder.cs | 6 +- .../Content/Tree/ContentFolderTreeNode.cs | 408 +++++++++++ .../Content/Tree/ContentItemTreeNode.cs | 221 ++++++ Source/Editor/Content/Tree/ContentTreeNode.cs | 333 --------- .../Content/Tree/MainContentTreeNode.cs | 8 +- Source/Editor/Content/Tree/ProjectTreeNode.cs | 39 +- .../Content/Tree/RootContentTreeNode.cs | 8 +- Source/Editor/GUI/NavigationBar.cs | 18 +- .../Editor/Modules/ContentDatabaseModule.cs | 38 +- .../Windows/ContentWindow.ContextMenu.cs | 12 +- .../Windows/ContentWindow.Navigation.cs | 95 ++- Source/Editor/Windows/ContentWindow.Search.cs | 11 +- Source/Editor/Windows/ContentWindow.cs | 649 ++++++++++++++++-- 14 files changed, 1394 insertions(+), 460 deletions(-) create mode 100644 Source/Editor/Content/Tree/ContentFolderTreeNode.cs create mode 100644 Source/Editor/Content/Tree/ContentItemTreeNode.cs delete mode 100644 Source/Editor/Content/Tree/ContentTreeNode.cs diff --git a/Source/Editor/Content/GUI/ContentNavigation.cs b/Source/Editor/Content/GUI/ContentNavigation.cs index a8581142b..c84b98746 100644 --- a/Source/Editor/Content/GUI/ContentNavigation.cs +++ b/Source/Editor/Content/GUI/ContentNavigation.cs @@ -19,7 +19,7 @@ namespace FlaxEditor.Content.GUI /// /// Gets the target node. /// - public ContentTreeNode TargetNode { get; } + public ContentFolderTreeNode TargetNode { get; } /// /// Initializes a new instance of the class. @@ -28,7 +28,7 @@ namespace FlaxEditor.Content.GUI /// The x position. /// The y position. /// The height. - public ContentNavigationButton(ContentTreeNode targetNode, float x, float y, float height) + public ContentNavigationButton(ContentFolderTreeNode targetNode, float x, float y, float height) : base(x, y, height) { TargetNode = targetNode; @@ -147,7 +147,7 @@ namespace FlaxEditor.Content.GUI ClearItems(); foreach (var child in Target.TargetNode.Children) { - if (child is ContentTreeNode node) + if (child is ContentFolderTreeNode node) { if (node.Folder.VisibleInHierarchy) // Respect the filter set by ContentFilterConfig.Filter(...) AddItem(node.Folder.ShortName); @@ -180,7 +180,7 @@ namespace FlaxEditor.Content.GUI var item = _items[index]; foreach (var child in Target.TargetNode.Children) { - if (child is ContentTreeNode node && node.Folder.ShortName == item) + if (child is ContentFolderTreeNode node && node.Folder.ShortName == item) { Editor.Instance.Windows.ContentWin.Navigate(node); return; diff --git a/Source/Editor/Content/Items/ContentFolder.cs b/Source/Editor/Content/Items/ContentFolder.cs index 2bc4c03b3..463dd8341 100644 --- a/Source/Editor/Content/Items/ContentFolder.cs +++ b/Source/Editor/Content/Items/ContentFolder.cs @@ -59,7 +59,7 @@ namespace FlaxEditor.Content /// /// Gets the content node. /// - public ContentTreeNode Node { get; } + public ContentFolderTreeNode Node { get; } /// /// The subitems of this folder. @@ -72,7 +72,7 @@ namespace FlaxEditor.Content /// The folder type. /// The path to the item. /// The folder parent node. - internal ContentFolder(ContentFolderType type, string path, ContentTreeNode node) + internal ContentFolder(ContentFolderType type, string path, ContentFolderTreeNode node) : base(path) { FolderType = type; @@ -118,7 +118,7 @@ namespace FlaxEditor.Content get { var hasParentFolder = ParentFolder != null; - var isContentFolder = Node is MainContentTreeNode; + var isContentFolder = Node is MainContentFolderTreeNode; return hasParentFolder && !isContentFolder; } } diff --git a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs new file mode 100644 index 000000000..93fa1f0d3 --- /dev/null +++ b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs @@ -0,0 +1,408 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using FlaxEditor.GUI; +using FlaxEditor.GUI.Drag; +using FlaxEditor.GUI.Tree; +using FlaxEditor.SceneGraph; +using FlaxEditor.Utilities; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content; + +/// +/// Content folder tree node. +/// +/// +public class ContentFolderTreeNode : TreeNode +{ + private DragItems _dragOverItems; + private DragActors _dragActors; + private List _highlights; + + /// + /// The folder. + /// + protected ContentFolder _folder; + + /// + /// Whether this node can be deleted. + /// + public virtual bool CanDelete => true; + + /// + /// Whether this node can be duplicated. + /// + public virtual bool CanDuplicate => true; + + /// + /// Gets the content folder item. + /// + public ContentFolder Folder => _folder; + + /// + /// Gets the type of the folder. + /// + public ContentFolderType FolderType => _folder.FolderType; + + /// + /// Returns true if that folder can import/manage scripts. + /// + public bool CanHaveScripts => _folder.CanHaveScripts; + + /// + /// Returns true if that folder can import/manage assets. + /// + /// True if can contain assets for project, otherwise false + public bool CanHaveAssets => _folder.CanHaveAssets; + + /// + /// Gets the parent node. + /// + public ContentFolderTreeNode ParentNode => Parent as ContentFolderTreeNode; + + /// + /// Gets the folder path. + /// + public string Path => _folder.Path; + + /// + /// Gets the navigation button label. + /// + public virtual string NavButtonLabel => _folder.ShortName; + + /// + /// Initializes a new instance of the class. + /// + /// The parent node. + /// The folder path. + public ContentFolderTreeNode(ContentFolderTreeNode parent, string path) + : this(parent, parent?.FolderType ?? ContentFolderType.Other, path) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The parent node. + /// The folder type. + /// The folder path. + protected ContentFolderTreeNode(ContentFolderTreeNode parent, ContentFolderType type, string path) + : base(false, Editor.Instance.Icons.FolderClosed32, Editor.Instance.Icons.FolderOpen32) + { + _folder = new ContentFolder(type, path, this); + Text = _folder.ShortName; + if (parent != null) + { + Folder.ParentFolder = parent.Folder; + Parent = parent; + } + IconColor = Color.Transparent; // Hide default icon, we draw scaled icon manually + Editor.Instance?.Windows?.ContentWin?.TryAutoExpandContentNode(this); + } + + /// + /// Shows the rename popup for the item. + /// + public void StartRenaming() + { + if (!_folder.CanRename) + return; + + // Start renaming the folder + Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(false); + var dialog = RenamePopup.Show(this, TextRect, _folder.ShortName, false); + dialog.Tag = _folder; + dialog.Renamed += popup => + { + Editor.Instance.Windows.ContentWin.Rename((ContentFolder)popup.Tag, popup.Text); + Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); + }; + dialog.Closed += popup => { Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); }; + } + + /// + /// Updates the query search filter. + /// + /// The filter text. + public void UpdateFilter(string filterText) + { + bool noFilter = string.IsNullOrWhiteSpace(filterText); + + // Update itself + bool isThisVisible; + if (noFilter) + { + // Clear filter + _highlights?.Clear(); + isThisVisible = true; + } + else + { + var text = Text; + if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges)) + { + // Update highlights + if (_highlights == null) + _highlights = new List(ranges.Length); + else + _highlights.Clear(); + var style = Style.Current; + var font = style.FontSmall; + var textRect = TextRect; + for (int i = 0; i < ranges.Length; i++) + { + var start = font.GetCharPosition(text, ranges[i].StartIndex); + var end = font.GetCharPosition(text, ranges[i].EndIndex); + _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); + } + isThisVisible = true; + } + else + { + // Hide + _highlights?.Clear(); + isThisVisible = false; + } + } + + // Update children + bool isAnyChildVisible = false; + for (int i = 0; i < _children.Count; i++) + { + if (_children[i] is ContentFolderTreeNode child) + { + child.UpdateFilter(filterText); + isAnyChildVisible |= child.Visible; + } + else if (_children[i] is ContentItemTreeNode itemNode) + { + itemNode.UpdateFilter(filterText); + isAnyChildVisible |= itemNode.Visible; + } + } + + bool isExpanded = isAnyChildVisible; + + if (isExpanded) + { + Expand(true); + } + else + { + Collapse(true); + } + + Visible = isThisVisible | isAnyChildVisible; + } + + /// + public override int Compare(Control other) + { + if (other is ContentItemTreeNode) + return -1; + if (other is ContentFolderTreeNode otherNode) + return string.Compare(Text, otherNode.Text, StringComparison.Ordinal); + return base.Compare(other); + } + + /// + public override void Draw() + { + base.Draw(); + + // Draw all highlights + if (_highlights != null) + { + var style = Style.Current; + var color = style.ProgressNormal * 0.6f; + for (int i = 0; i < _highlights.Count; i++) + Render2D.FillRectangle(_highlights[i], color); + } + + var contentWindow = Editor.Instance.Windows.ContentWin; + var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f; + var icon = IsExpanded ? Editor.Instance.Icons.FolderOpen32 : Editor.Instance.Icons.FolderClosed32; + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var iconRect = new Rectangle(TextRect.Left - iconSize - 2.0f, (HeaderHeight - iconSize) * 0.5f, iconSize, iconSize); + Render2D.DrawSprite(icon, iconRect); + } + + /// + public override void OnDestroy() + { + // Delete folder item + _folder.Dispose(); + + base.OnDestroy(); + } + + /// + protected override void OnExpandedChanged() + { + base.OnExpandedChanged(); + + Editor.Instance?.Windows?.ContentWin?.OnContentTreeNodeExpandedChanged(this, IsExpanded); + } + + private DragDropEffect GetDragEffect(DragData data) + { + if (_dragActors != null && _dragActors.HasValidDrag) + return DragDropEffect.Move; + if (data is DragDataFiles) + { + if (_folder.CanHaveAssets) + return DragDropEffect.Copy; + } + else + { + if (_dragOverItems != null && _dragOverItems.HasValidDrag) + return DragDropEffect.Move; + } + + return DragDropEffect.None; + } + + private bool ValidateDragItem(ContentItem item) + { + // Reject itself and any parent + return item != _folder && !item.Find(_folder); + } + + private bool ValidateDragActors(ActorNode actor) + { + return actor.CanCreatePrefab && _folder.CanHaveAssets; + } + + private void ImportActors(DragActors actors) + { + Select(); + foreach (var actorNode in actors.Objects) + { + var actor = actorNode.Actor; + if (actors.Objects.Contains(actorNode.ParentNode as ActorNode)) + continue; + + Editor.Instance.Prefabs.CreatePrefab(actor, false); + } + Editor.Instance.Windows.ContentWin.RefreshView(); + } + + /// + protected override DragDropEffect OnDragEnterHeader(DragData data) + { + if (data is DragDataFiles) + return _folder.CanHaveAssets ? DragDropEffect.Copy : DragDropEffect.None; + + if (_dragActors == null) + _dragActors = new DragActors(ValidateDragActors); + if (_dragActors.OnDragEnter(data)) + return DragDropEffect.Move; + + if (_dragOverItems == null) + _dragOverItems = new DragItems(ValidateDragItem); + + _dragOverItems.OnDragEnter(data); + return GetDragEffect(data); + } + + /// + protected override DragDropEffect OnDragMoveHeader(DragData data) + { + if (data is DragDataFiles) + return _folder.CanHaveAssets ? DragDropEffect.Copy : DragDropEffect.None; + if (_dragActors != null && _dragActors.HasValidDrag) + return DragDropEffect.Move; + return GetDragEffect(data); + } + + /// + protected override void OnDragLeaveHeader() + { + _dragOverItems?.OnDragLeave(); + _dragActors?.OnDragLeave(); + base.OnDragLeaveHeader(); + } + + /// + protected override DragDropEffect OnDragDropHeader(DragData data) + { + var result = DragDropEffect.None; + + // Check if drop element or files + if (data is DragDataFiles files) + { + // Import files + Editor.Instance.ContentImporting.Import(files.Files, _folder); + result = DragDropEffect.Copy; + + Expand(); + } + else if (_dragActors != null && _dragActors.HasValidDrag) + { + ImportActors(_dragActors); + _dragActors.OnDragDrop(); + result = DragDropEffect.Move; + + Expand(); + } + else if (_dragOverItems != null && _dragOverItems.HasValidDrag) + { + // Move items + Editor.Instance.ContentDatabase.Move(_dragOverItems.Objects, _folder); + result = DragDropEffect.Move; + + Expand(); + } + + _dragOverItems?.OnDragDrop(); + + return result; + } + + /// + protected override void DoDragDrop() + { + DoDragDrop(DragItems.GetDragData(_folder)); + } + + /// + protected override void OnLongPress() + { + Select(); + + StartRenaming(); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (IsFocused) + { + switch (key) + { + case KeyboardKeys.F2: + StartRenaming(); + return true; + case KeyboardKeys.Delete: + if (Folder.Exists && CanDelete) + Editor.Instance.Windows.ContentWin.Delete(Folder); + return true; + } + if (RootWindow.GetKey(KeyboardKeys.Control)) + { + switch (key) + { + case KeyboardKeys.D: + if (Folder.Exists && CanDuplicate) + Editor.Instance.Windows.ContentWin.Duplicate(Folder); + return true; + } + } + } + + return base.OnKeyDown(key); + } +} diff --git a/Source/Editor/Content/Tree/ContentItemTreeNode.cs b/Source/Editor/Content/Tree/ContentItemTreeNode.cs new file mode 100644 index 000000000..43dbe278a --- /dev/null +++ b/Source/Editor/Content/Tree/ContentItemTreeNode.cs @@ -0,0 +1,221 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using FlaxEditor.Content.GUI; +using FlaxEditor.GUI.Drag; +using FlaxEditor.GUI.Tree; +using FlaxEditor.Utilities; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content; + +/// +/// Tree node for non-folder content items. +/// +public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner +{ + private List _highlights; + + /// + /// The content item. + /// + public ContentItem Item { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The content item. + public ContentItemTreeNode(ContentItem item) + : base(false, Editor.Instance.Icons.Document128, Editor.Instance.Icons.Document128) + { + Item = item ?? throw new ArgumentNullException(nameof(item)); + UpdateDisplayedName(); + IconColor = Color.Transparent; // Reserve icon space but draw custom thumbnail. + Item.AddReference(this); + } + + private static SpriteHandle GetIcon(ContentItem item) + { + if (item == null) + return SpriteHandle.Invalid; + var icon = item.Thumbnail; + if (!icon.IsValid) + icon = item.DefaultThumbnail; + if (!icon.IsValid) + icon = Editor.Instance.Icons.Document128; + return icon; + } + + /// + /// Updates the query search filter. + /// + /// The filter text. + public void UpdateFilter(string filterText) + { + bool noFilter = string.IsNullOrWhiteSpace(filterText); + bool isVisible; + if (noFilter) + { + _highlights?.Clear(); + isVisible = true; + } + else + { + var text = Text; + if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges)) + { + if (_highlights == null) + _highlights = new List(ranges.Length); + else + _highlights.Clear(); + var style = Style.Current; + var font = style.FontSmall; + var textRect = TextRect; + for (int i = 0; i < ranges.Length; i++) + { + var start = font.GetCharPosition(text, ranges[i].StartIndex); + var end = font.GetCharPosition(text, ranges[i].EndIndex); + _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); + } + isVisible = true; + } + else + { + _highlights?.Clear(); + isVisible = false; + } + } + + Visible = isVisible; + } + + /// + public override void Draw() + { + base.Draw(); + + var icon = GetIcon(Item); + if (icon.IsValid) + { + var contentWindow = Editor.Instance.Windows.ContentWin; + var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f; + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var textRect = TextRect; + var iconRect = new Rectangle(textRect.Left - iconSize - 2.0f, (HeaderHeight - iconSize) * 0.5f, iconSize, iconSize); + Render2D.DrawSprite(icon, iconRect); + } + + if (_highlights != null) + { + var style = Style.Current; + var color = style.ProgressNormal * 0.6f; + for (int i = 0; i < _highlights.Count; i++) + Render2D.FillRectangle(_highlights[i], color); + } + } + + /// + protected override bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button) + { + if (button == MouseButton.Left) + { + Editor.Instance.Windows.ContentWin.Open(Item); + return true; + } + + return base.OnMouseDoubleClickHeader(ref location, button); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (IsFocused) + { + switch (key) + { + case KeyboardKeys.Return: + Editor.Instance.Windows.ContentWin.Open(Item); + return true; + case KeyboardKeys.F2: + Editor.Instance.Windows.ContentWin.Rename(Item); + return true; + case KeyboardKeys.Delete: + Editor.Instance.Windows.ContentWin.Delete(Item); + return true; + } + } + + return base.OnKeyDown(key); + } + + /// + protected override void DoDragDrop() + { + DoDragDrop(DragItems.GetDragData(Item)); + } + + /// + public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) + { + Item.UpdateTooltipText(); + TooltipText = Item.TooltipText; + return base.OnShowTooltip(out text, out location, out area); + } + + /// + void IContentItemOwner.OnItemDeleted(ContentItem item) + { + } + + /// + void IContentItemOwner.OnItemRenamed(ContentItem item) + { + UpdateDisplayedName(); + } + + /// + void IContentItemOwner.OnItemReimported(ContentItem item) + { + } + + /// + void IContentItemOwner.OnItemDispose(ContentItem item) + { + } + + /// + public override int Compare(Control other) + { + if (other is ContentFolderTreeNode) + return 1; + if (other is ContentItemTreeNode otherItem) + return ApplySortOrder(string.Compare(Text, otherItem.Text, StringComparison.InvariantCulture)); + return base.Compare(other); + } + + /// + public override void OnDestroy() + { + Item.RemoveReference(this); + base.OnDestroy(); + } + + public void UpdateDisplayedName() + { + var contentWindow = Editor.Instance?.Windows?.ContentWin; + var showExtensions = contentWindow?.View?.ShowFileExtensions ?? true; + Text = Item.ShowFileExtension || showExtensions ? Item.FileName : Item.ShortName; + } + + private static SortType GetSortType() + { + return Editor.Instance?.Windows?.ContentWin?.CurrentSortType ?? SortType.AlphabeticOrder; + } + + private static int ApplySortOrder(int result) + { + return GetSortType() == SortType.AlphabeticReverse ? -result : result; + } +} diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs deleted file mode 100644 index 8629296fc..000000000 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) Wojciech Figat. All rights reserved. - -using System.Collections.Generic; -using FlaxEditor.GUI; -using FlaxEditor.GUI.Drag; -using FlaxEditor.GUI.Tree; -using FlaxEditor.Utilities; -using FlaxEngine; -using FlaxEngine.GUI; - -namespace FlaxEditor.Content -{ - /// - /// Content folder tree node. - /// - /// - public class ContentTreeNode : TreeNode - { - private DragItems _dragOverItems; - private List _highlights; - - /// - /// The folder. - /// - protected ContentFolder _folder; - - /// - /// Whether this node can be deleted. - /// - public virtual bool CanDelete => true; - - /// - /// Whether this node can be duplicated. - /// - public virtual bool CanDuplicate => true; - - /// - /// Gets the content folder item. - /// - public ContentFolder Folder => _folder; - - /// - /// Gets the type of the folder. - /// - public ContentFolderType FolderType => _folder.FolderType; - - /// - /// Returns true if that folder can import/manage scripts. - /// - public bool CanHaveScripts => _folder.CanHaveScripts; - - /// - /// Returns true if that folder can import/manage assets. - /// - /// True if can contain assets for project, otherwise false - public bool CanHaveAssets => _folder.CanHaveAssets; - - /// - /// Gets the parent node. - /// - public ContentTreeNode ParentNode => Parent as ContentTreeNode; - - /// - /// Gets the folder path. - /// - public string Path => _folder.Path; - - /// - /// Gets the navigation button label. - /// - public virtual string NavButtonLabel => _folder.ShortName; - - /// - /// Initializes a new instance of the class. - /// - /// The parent node. - /// The folder path. - public ContentTreeNode(ContentTreeNode parent, string path) - : this(parent, parent?.FolderType ?? ContentFolderType.Other, path) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The parent node. - /// The folder type. - /// The folder path. - protected ContentTreeNode(ContentTreeNode parent, ContentFolderType type, string path) - : base(false, Editor.Instance.Icons.FolderClosed32, Editor.Instance.Icons.FolderOpen32) - { - _folder = new ContentFolder(type, path, this); - Text = _folder.ShortName; - if (parent != null) - { - Folder.ParentFolder = parent.Folder; - Parent = parent; - } - IconColor = Style.Current.Foreground; - } - - /// - /// Shows the rename popup for the item. - /// - public void StartRenaming() - { - if (!_folder.CanRename) - return; - - // Start renaming the folder - Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(false); - var dialog = RenamePopup.Show(this, TextRect, _folder.ShortName, false); - dialog.Tag = _folder; - dialog.Renamed += popup => - { - Editor.Instance.Windows.ContentWin.Rename((ContentFolder)popup.Tag, popup.Text); - Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); - }; - dialog.Closed += popup => { Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); }; - } - - /// - /// Updates the query search filter. - /// - /// The filter text. - public void UpdateFilter(string filterText) - { - bool noFilter = string.IsNullOrWhiteSpace(filterText); - - // Update itself - bool isThisVisible; - if (noFilter) - { - // Clear filter - _highlights?.Clear(); - isThisVisible = true; - } - else - { - var text = Text; - if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges)) - { - // Update highlights - if (_highlights == null) - _highlights = new List(ranges.Length); - else - _highlights.Clear(); - var style = Style.Current; - var font = style.FontSmall; - var textRect = TextRect; - for (int i = 0; i < ranges.Length; i++) - { - var start = font.GetCharPosition(text, ranges[i].StartIndex); - var end = font.GetCharPosition(text, ranges[i].EndIndex); - _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); - } - isThisVisible = true; - } - else - { - // Hide - _highlights?.Clear(); - isThisVisible = false; - } - } - - // Update children - bool isAnyChildVisible = false; - for (int i = 0; i < _children.Count; i++) - { - if (_children[i] is ContentTreeNode child) - { - child.UpdateFilter(filterText); - isAnyChildVisible |= child.Visible; - } - } - - bool isExpanded = isAnyChildVisible; - - if (isExpanded) - { - Expand(true); - } - else - { - Collapse(true); - } - - Visible = isThisVisible | isAnyChildVisible; - } - - /// - public override void Draw() - { - base.Draw(); - - // Draw all highlights - if (_highlights != null) - { - var style = Style.Current; - var color = style.ProgressNormal * 0.6f; - for (int i = 0; i < _highlights.Count; i++) - Render2D.FillRectangle(_highlights[i], color); - } - } - - /// - public override void OnDestroy() - { - // Delete folder item - _folder.Dispose(); - - base.OnDestroy(); - } - - private DragDropEffect GetDragEffect(DragData data) - { - if (data is DragDataFiles) - { - if (_folder.CanHaveAssets) - return DragDropEffect.Copy; - } - else - { - if (_dragOverItems.HasValidDrag) - return DragDropEffect.Move; - } - - return DragDropEffect.None; - } - - private bool ValidateDragItem(ContentItem item) - { - // Reject itself and any parent - return item != _folder && !item.Find(_folder); - } - - /// - protected override DragDropEffect OnDragEnterHeader(DragData data) - { - if (_dragOverItems == null) - _dragOverItems = new DragItems(ValidateDragItem); - - _dragOverItems.OnDragEnter(data); - return GetDragEffect(data); - } - - /// - protected override DragDropEffect OnDragMoveHeader(DragData data) - { - return GetDragEffect(data); - } - - /// - protected override void OnDragLeaveHeader() - { - _dragOverItems.OnDragLeave(); - base.OnDragLeaveHeader(); - } - - /// - protected override DragDropEffect OnDragDropHeader(DragData data) - { - var result = DragDropEffect.None; - - // Check if drop element or files - if (data is DragDataFiles files) - { - // Import files - Editor.Instance.ContentImporting.Import(files.Files, _folder); - result = DragDropEffect.Copy; - - Expand(); - } - else if (_dragOverItems.HasValidDrag) - { - // Move items - Editor.Instance.ContentDatabase.Move(_dragOverItems.Objects, _folder); - result = DragDropEffect.Move; - - Expand(); - } - - _dragOverItems.OnDragDrop(); - - return result; - } - - /// - protected override void DoDragDrop() - { - DoDragDrop(DragItems.GetDragData(_folder)); - } - - /// - protected override void OnLongPress() - { - Select(); - - StartRenaming(); - } - - /// - public override bool OnKeyDown(KeyboardKeys key) - { - if (IsFocused) - { - switch (key) - { - case KeyboardKeys.F2: - StartRenaming(); - return true; - case KeyboardKeys.Delete: - if (Folder.Exists && CanDelete) - Editor.Instance.Windows.ContentWin.Delete(Folder); - return true; - } - if (RootWindow.GetKey(KeyboardKeys.Control)) - { - switch (key) - { - case KeyboardKeys.D: - if (Folder.Exists && CanDuplicate) - Editor.Instance.Windows.ContentWin.Duplicate(Folder); - return true; - } - } - } - - return base.OnKeyDown(key); - } - } -} diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index fc147f5c9..aaeed0eda 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -7,8 +7,8 @@ namespace FlaxEditor.Content /// /// Content tree node used for main directories. /// - /// - public class MainContentTreeNode : ContentTreeNode + /// + public class MainContentFolderTreeNode : ContentFolderTreeNode { private FileSystemWatcher _watcher; @@ -19,12 +19,12 @@ namespace FlaxEditor.Content public override bool CanDuplicate => false; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent project. /// The folder type. /// The folder path. - public MainContentTreeNode(ProjectTreeNode parent, ContentFolderType type, string path) + public MainContentFolderTreeNode(ProjectFolderTreeNode parent, ContentFolderType type, string path) : base(parent, type, path) { _watcher = new FileSystemWatcher(path) diff --git a/Source/Editor/Content/Tree/ProjectTreeNode.cs b/Source/Editor/Content/Tree/ProjectTreeNode.cs index 61002d4fa..4f921e371 100644 --- a/Source/Editor/Content/Tree/ProjectTreeNode.cs +++ b/Source/Editor/Content/Tree/ProjectTreeNode.cs @@ -1,5 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using FlaxEngine.GUI; namespace FlaxEditor.Content @@ -7,8 +8,8 @@ namespace FlaxEditor.Content /// /// Root tree node for the project workspace. /// - /// - public sealed class ProjectTreeNode : ContentTreeNode + /// + public sealed class ProjectFolderTreeNode : ContentFolderTreeNode { /// /// The project/ @@ -18,18 +19,18 @@ namespace FlaxEditor.Content /// /// The project content directory. /// - public MainContentTreeNode Content; + public MainContentFolderTreeNode Content; /// /// The project source code directory. /// - public MainContentTreeNode Source; + public MainContentFolderTreeNode Source; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The project. - public ProjectTreeNode(ProjectInfo project) + public ProjectFolderTreeNode(ProjectInfo project) : base(null, project.ProjectFolderPath) { Project = project; @@ -48,9 +49,29 @@ namespace FlaxEditor.Content /// public override int Compare(Control other) { - // Move the main game project to the top - if (Project.Name == Editor.Instance.GameProject.Name) - return -1; + if (other is ProjectFolderTreeNode otherProject) + { + var gameProject = Editor.Instance.GameProject; + var engineProject = Editor.Instance.EngineProject; + bool isGame = Project == gameProject; + bool isEngine = Project == engineProject; + bool otherIsGame = otherProject.Project == gameProject; + bool otherIsEngine = otherProject.Project == engineProject; + + // Main game project at the top + if (isGame && !otherIsGame) + return -1; + if (!isGame && otherIsGame) + return 1; + + // Engine project at the bottom (when distinct) + if (isEngine && !otherIsEngine) + return 1; + if (!isEngine && otherIsEngine) + return -1; + + return string.CompareOrdinal(Project.Name, otherProject.Project.Name); + } return base.Compare(other); } } diff --git a/Source/Editor/Content/Tree/RootContentTreeNode.cs b/Source/Editor/Content/Tree/RootContentTreeNode.cs index d9c1ba761..d1f816c14 100644 --- a/Source/Editor/Content/Tree/RootContentTreeNode.cs +++ b/Source/Editor/Content/Tree/RootContentTreeNode.cs @@ -5,13 +5,13 @@ namespace FlaxEditor.Content /// /// Root tree node for the content workspace. /// - /// - public sealed class RootContentTreeNode : ContentTreeNode + /// + public sealed class RootContentFolderTreeNode : ContentFolderTreeNode { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public RootContentTreeNode() + public RootContentFolderTreeNode() : base(null, string.Empty) { } diff --git a/Source/Editor/GUI/NavigationBar.cs b/Source/Editor/GUI/NavigationBar.cs index eb87eaafc..7f43d09ac 100644 --- a/Source/Editor/GUI/NavigationBar.cs +++ b/Source/Editor/GUI/NavigationBar.cs @@ -76,9 +76,23 @@ namespace FlaxEditor.GUI toolstrip.IsLayoutLocked = toolstripLocked; toolstrip.ItemsMargin = toolstripMargin; - var lastToolstripButton = toolstrip.LastButton; + var margin = toolstrip.ItemsMargin; + float xOffset = margin.Left; + bool hadChild = false; + for (int i = 0; i < toolstrip.ChildrenCount; i++) + { + var child = toolstrip.GetChild(i); + if (child == this || !child.Visible) + continue; + hadChild = true; + xOffset += child.Width + margin.Width; + } + + var right = hadChild ? xOffset - margin.Width : margin.Left; var parentSize = Parent.Size; - Bounds = new Rectangle(lastToolstripButton.Right + 8.0f, 0, parentSize.X - X - 8.0f, toolstrip.Height); + var x = right + 8.0f; + var width = Mathf.Max(parentSize.X - x - 8.0f, 0.0f); + Bounds = new Rectangle(x, 0, width, toolstrip.Height); } /// diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 53e45ff25..db2bdd7e6 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -24,22 +24,22 @@ namespace FlaxEditor.Modules private bool _rebuildInitFlag; private int _itemsCreated; private int _itemsDeleted; - private readonly HashSet _dirtyNodes = new HashSet(); + private readonly HashSet _dirtyNodes = new HashSet(); /// /// The project directory. /// - public ProjectTreeNode Game { get; private set; } + public ProjectFolderTreeNode Game { get; private set; } /// /// The engine directory. /// - public ProjectTreeNode Engine { get; private set; } + public ProjectFolderTreeNode Engine { get; private set; } /// /// The list of all projects workspace directories (including game, engine and plugins projects). /// - public readonly List Projects = new List(); + public readonly List Projects = new List(); /// /// The list with all content items proxy objects. Use and to modify this or to refresh database when adding new item proxy types. @@ -116,7 +116,7 @@ namespace FlaxEditor.Modules /// /// The project. /// The project workspace or null if not loaded into database. - public ProjectTreeNode GetProjectWorkspace(ProjectInfo project) + public ProjectFolderTreeNode GetProjectWorkspace(ProjectInfo project) { return Projects.FirstOrDefault(x => x.Project == project); } @@ -874,7 +874,7 @@ namespace FlaxEditor.Modules } } - private void LoadFolder(ContentTreeNode node, bool checkSubDirs) + private void LoadFolder(ContentFolderTreeNode node, bool checkSubDirs) { if (node == null) return; @@ -953,7 +953,7 @@ namespace FlaxEditor.Modules if (childFolderNode == null) { // Create node - ContentTreeNode n = new ContentTreeNode(node, childPath); + ContentFolderTreeNode n = new ContentFolderTreeNode(node, childPath); if (!_isDuringFastSetup) sortChildren = true; @@ -978,7 +978,7 @@ namespace FlaxEditor.Modules node.SortChildren(); // Ignore some special folders - if (node is MainContentTreeNode mainNode && mainNode.Folder.ShortName == "Source") + if (node is MainContentFolderTreeNode mainNode && mainNode.Folder.ShortName == "Source") { var mainNodeChild = mainNode.Folder.Find(StringUtils.CombinePaths(mainNode.Path, "obj")) as ContentFolder; if (mainNodeChild != null) @@ -995,7 +995,7 @@ namespace FlaxEditor.Modules } } - private void LoadScripts(ContentTreeNode parent, string[] files) + private void LoadScripts(ContentFolderTreeNode parent, string[] files) { for (int i = 0; i < files.Length; i++) { @@ -1041,7 +1041,7 @@ namespace FlaxEditor.Modules } } - private void LoadAssets(ContentTreeNode parent, string[] files) + private void LoadAssets(ContentFolderTreeNode parent, string[] files) { for (int i = 0; i < files.Length; i++) { @@ -1093,20 +1093,20 @@ namespace FlaxEditor.Modules var workspace = GetProjectWorkspace(project); if (workspace == null) { - workspace = new ProjectTreeNode(project); + workspace = new ProjectFolderTreeNode(project); Projects.Add(workspace); var contentFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Content"); if (Directory.Exists(contentFolder)) { - workspace.Content = new MainContentTreeNode(workspace, ContentFolderType.Content, contentFolder); + workspace.Content = new MainContentFolderTreeNode(workspace, ContentFolderType.Content, contentFolder); workspace.Content.Folder.ParentFolder = workspace.Folder; } var sourceFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Source"); if (Directory.Exists(sourceFolder)) { - workspace.Source = new MainContentTreeNode(workspace, ContentFolderType.Source, sourceFolder); + workspace.Source = new MainContentFolderTreeNode(workspace, ContentFolderType.Source, sourceFolder); workspace.Source.Folder.ParentFolder = workspace.Folder; } } @@ -1213,16 +1213,16 @@ namespace FlaxEditor.Modules Proxy.Add(new GenericJsonAssetProxy()); // Create content folders nodes - Engine = new ProjectTreeNode(Editor.EngineProject) + Engine = new ProjectFolderTreeNode(Editor.EngineProject) { - Content = new MainContentTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder), + Content = new MainContentFolderTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder), }; if (Editor.GameProject != Editor.EngineProject) { - Game = new ProjectTreeNode(Editor.GameProject) + Game = new ProjectFolderTreeNode(Editor.GameProject) { - Content = new MainContentTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder), - Source = new MainContentTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder), + Content = new MainContentFolderTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder), + Source = new MainContentFolderTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder), }; // TODO: why it's required? the code above should work for linking the nodes hierarchy Game.Content.Folder.ParentFolder = Game.Folder; @@ -1302,7 +1302,7 @@ namespace FlaxEditor.Modules } } - internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e) + internal void OnDirectoryEvent(MainContentFolderTreeNode node, FileSystemEventArgs e) { // Ensure to be ready for external events if (_isDuringFastSetup) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 1ad225997..28364e6dd 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -59,7 +59,7 @@ namespace FlaxEditor.Windows cm.AddSeparator(); } - if (item is ContentFolder contentFolder && contentFolder.Node is ProjectTreeNode) + if (item is ContentFolder contentFolder && contentFolder.Node is ProjectFolderTreeNode) { cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); } @@ -77,7 +77,7 @@ namespace FlaxEditor.Windows cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path))); - if (!String.IsNullOrEmpty(Editor.Instance.Windows.ContentWin._itemsSearchBox.Text)) + if (!_showAllContentInTree && !String.IsNullOrEmpty(Editor.Instance.Windows.ContentWin._itemsSearchBox.Text)) { cm.AddButton("Show in Content Panel", () => { @@ -130,7 +130,7 @@ namespace FlaxEditor.Windows } } - if (isFolder && folder.Node is MainContentTreeNode) + if (isFolder && folder.Node is MainContentFolderTreeNode) { cm.AddSeparator(); } @@ -146,7 +146,7 @@ namespace FlaxEditor.Windows b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); - if (isFolder && folder.Node is MainContentTreeNode) + if (isFolder && folder.Node is MainContentFolderTreeNode) { // Do nothing } @@ -179,14 +179,14 @@ namespace FlaxEditor.Windows cm.AddSeparator(); // Check if is source folder to add new module - if (folder?.ParentFolder?.Node is ProjectTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) + if (folder?.ParentFolder?.Node is ProjectFolderTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) { var button = cm.AddButton("New module"); button.CloseMenuOnClick = false; button.Clicked += () => NewModule(button, parentFolderNode.Source.Path); } - if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode)) + if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode)) { cm.AddButton("New folder", NewFolder); } diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs index 53dd6447b..cc9429d47 100644 --- a/Source/Editor/Windows/ContentWindow.Navigation.cs +++ b/Source/Editor/Windows/ContentWindow.Navigation.cs @@ -10,15 +10,41 @@ namespace FlaxEditor.Windows { public partial class ContentWindow { - private static readonly List NavUpdateCache = new List(8); + private static readonly List NavUpdateCache = new List(8); private void OnTreeSelectionChanged(List from, List to) { + if (!_showAllContentInTree && to.Count > 1) + { + _tree.Select(to[^1]); + return; + } + if (_showAllContentInTree && to.Count > 1) + { + var activeNode = GetActiveTreeSelection(to); + if (activeNode is ContentItemTreeNode itemNode) + SaveLastViewedFolder(itemNode.Item?.ParentFolder?.Node); + else + SaveLastViewedFolder(activeNode as ContentFolderTreeNode); + UpdateUI(); + return; + } + // Navigate - var source = from.Count > 0 ? from[0] as ContentTreeNode : null; - var target = to.Count > 0 ? to[0] as ContentTreeNode : null; + var source = from.Count > 0 ? from[0] as ContentFolderTreeNode : null; + var targetNode = GetActiveTreeSelection(to); + if (targetNode is ContentItemTreeNode itemNode2) + { + SaveLastViewedFolder(itemNode2.Item?.ParentFolder?.Node); + UpdateUI(); + itemNode2.Focus(); + return; + } + + var target = targetNode as ContentFolderTreeNode; Navigate(source, target); + SaveLastViewedFolder(target); target?.Focus(); } @@ -26,12 +52,12 @@ namespace FlaxEditor.Windows /// Navigates to the specified target content location. /// /// The target. - public void Navigate(ContentTreeNode target) + public void Navigate(ContentFolderTreeNode target) { Navigate(SelectedNode, target); } - private void Navigate(ContentTreeNode source, ContentTreeNode target) + private void Navigate(ContentFolderTreeNode source, ContentFolderTreeNode target) { if (target == null) target = _root; @@ -50,7 +76,8 @@ namespace FlaxEditor.Windows } // Show folder contents and select tree node - RefreshView(target); + if (!_showAllContentInTree) + RefreshView(target); _tree.Select(target); target.ExpandAllParents(); @@ -62,7 +89,8 @@ namespace FlaxEditor.Windows //UndoList.SetSize(32); // Update search - UpdateItemsSearch(); + if (!_showAllContentInTree) + UpdateItemsSearch(); // Unlock navigation _navigationUnlocked = true; @@ -81,7 +109,7 @@ namespace FlaxEditor.Windows if (_navigationUnlocked && _navigationUndo.Count > 0) { // Pop node - ContentTreeNode node = _navigationUndo.Pop(); + ContentFolderTreeNode node = _navigationUndo.Pop(); // Lock navigation _navigationUnlocked = false; @@ -90,7 +118,8 @@ namespace FlaxEditor.Windows _navigationRedo.Push(SelectedNode); // Select node - RefreshView(node); + if (!_showAllContentInTree) + RefreshView(node); _tree.Select(node); node.ExpandAllParents(); @@ -99,14 +128,16 @@ namespace FlaxEditor.Windows //UndoList.SetSize(32); // Update search - UpdateItemsSearch(); + if (!_showAllContentInTree) + UpdateItemsSearch(); // Unlock navigation _navigationUnlocked = true; // Update UI UpdateUI(); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); } } @@ -119,7 +150,7 @@ namespace FlaxEditor.Windows if (_navigationUnlocked && _navigationRedo.Count > 0) { // Pop node - ContentTreeNode node = _navigationRedo.Pop(); + ContentFolderTreeNode node = _navigationRedo.Pop(); // Lock navigation _navigationUnlocked = false; @@ -128,7 +159,8 @@ namespace FlaxEditor.Windows _navigationUndo.Push(SelectedNode); // Select node - RefreshView(node); + if (!_showAllContentInTree) + RefreshView(node); _tree.Select(node); node.ExpandAllParents(); @@ -137,14 +169,16 @@ namespace FlaxEditor.Windows //UndoList.SetSize(32); // Update search - UpdateItemsSearch(); + if (!_showAllContentInTree) + UpdateItemsSearch(); // Unlock navigation _navigationUnlocked = true; // Update UI UpdateUI(); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); } } @@ -153,8 +187,8 @@ namespace FlaxEditor.Windows /// public void NavigateUp() { - ContentTreeNode target = _root; - ContentTreeNode current = SelectedNode; + ContentFolderTreeNode target = _root; + ContentFolderTreeNode current = SelectedNode; if (current?.Folder.ParentFolder != null) { @@ -188,7 +222,7 @@ namespace FlaxEditor.Windows // Spawn buttons var nodes = NavUpdateCache; nodes.Clear(); - ContentTreeNode node = SelectedNode; + ContentFolderTreeNode node = SelectedNode; while (node != null) { nodes.Add(node); @@ -222,13 +256,31 @@ namespace FlaxEditor.Windows /// /// Gets the selected tree node. /// - public ContentTreeNode SelectedNode => _tree.SelectedNode as ContentTreeNode; + public ContentFolderTreeNode SelectedNode + { + get + { + var selected = GetActiveTreeSelection(_tree.Selection); + if (selected is ContentItemTreeNode itemNode) + return itemNode.Parent as ContentFolderTreeNode; + return selected as ContentFolderTreeNode; + } + } /// /// Gets the current view folder. /// public ContentFolder CurrentViewFolder => SelectedNode?.Folder; + private TreeNode GetActiveTreeSelection(List selection) + { + if (selection == null || selection.Count == 0) + return null; + return _showAllContentInTree && selection.Count > 1 + ? selection[^1] + : selection[0]; + } + /// /// Shows the root folder. /// @@ -236,5 +288,10 @@ namespace FlaxEditor.Windows { _tree.Select(_root); } + + private void SaveLastViewedFolder(ContentFolderTreeNode node) + { + Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, node?.Path ?? string.Empty); + } } } diff --git a/Source/Editor/Windows/ContentWindow.Search.cs b/Source/Editor/Windows/ContentWindow.Search.cs index f28dc4834..87564597a 100644 --- a/Source/Editor/Windows/ContentWindow.Search.cs +++ b/Source/Editor/Windows/ContentWindow.Search.cs @@ -115,11 +115,13 @@ namespace FlaxEditor.Windows var root = _root; root.LockChildrenRecursive(); + _suppressExpandedStateSave = true; // Update tree var query = _foldersSearchBox.Text; root.UpdateFilter(query); + _suppressExpandedStateSave = false; root.UnlockChildrenRecursive(); PerformLayout(); PerformLayout(); @@ -160,6 +162,11 @@ namespace FlaxEditor.Windows // Skip events during setup or init stuff if (IsLayoutLocked) return; + if (_showAllContentInTree) + { + RefreshTreeItems(); + return; + } // Check if clear filters if (_itemsSearchBox.TextLength == 0 && !_viewDropdown.HasSelection) @@ -199,7 +206,7 @@ namespace FlaxEditor.Windows // Special case for root folder for (int i = 0; i < _root.ChildrenCount; i++) { - if (_root.GetChild(i) is ContentTreeNode node) + if (_root.GetChild(i) is ContentFolderTreeNode node) UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles); } } @@ -221,7 +228,7 @@ namespace FlaxEditor.Windows // Special case for root folder for (int i = 0; i < _root.ChildrenCount; i++) { - if (_root.GetChild(i) is ContentTreeNode node) + if (_root.GetChild(i) is ContentFolderTreeNode node) UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles, query); } } diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 41382e60c..780be5c04 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -27,10 +27,15 @@ namespace FlaxEditor.Windows public sealed partial class ContentWindow : EditorWindow { private const string ProjectDataLastViewedFolder = "LastViewedFolder"; + private const string ProjectDataExpandedFolders = "ExpandedFolders"; private bool _isWorkspaceDirty; private string _workspaceRebuildLocation; private string _lastViewedFolderBeforeReload; private SplitPanel _split; + private Panel _treeOnlyPanel; + private ContainerControl _treePanelRoot; + private ContainerControl _treeHeaderPanel; + private Panel _contentItemsSearchPanel; private Panel _contentViewPanel; private Panel _contentTreePanel; private ContentView _view; @@ -42,18 +47,23 @@ namespace FlaxEditor.Windows private readonly ToolStripButton _navigateUpButton; private NavigationBar _navigationBar; + private Panel _viewDropdownPanel; private Tree _tree; private TextBox _foldersSearchBox; private TextBox _itemsSearchBox; private ViewDropdown _viewDropdown; private SortType _sortType; private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true, _showGeneratedFiles = false; + private bool _showAllContentInTree; + private bool _suppressExpandedStateSave; + private readonly HashSet _expandedFolderPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + private bool _renameInTree; - private RootContentTreeNode _root; + private RootContentFolderTreeNode _root; private bool _navigationUnlocked; - private readonly Stack _navigationUndo = new Stack(32); - private readonly Stack _navigationRedo = new Stack(32); + private readonly Stack _navigationUndo = new Stack(32); + private readonly Stack _navigationRedo = new Stack(32); private NewItem _newElement; @@ -133,6 +143,9 @@ namespace FlaxEditor.Windows } } + internal bool IsTreeOnlyMode => _showAllContentInTree; + internal SortType CurrentSortType => _sortType; + /// /// Initializes a new instance of the class. /// @@ -161,14 +174,6 @@ namespace FlaxEditor.Windows _navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up"); _toolStrip.AddSeparator(); - // Navigation bar - _navigationBar = new NavigationBar - { - Parent = _toolStrip, - ScrollbarTrackColor = style.Background, - ScrollbarThumbColor = style.ForegroundGrey, - }; - // Split panel _split = new SplitPanel(options.Options.Interface.ContentWindowOrientation, ScrollBars.None, ScrollBars.None) { @@ -178,19 +183,38 @@ namespace FlaxEditor.Windows Parent = this, }; + // Tree-only panel (used when showing all content in the tree) + _treeOnlyPanel = new Panel(ScrollBars.None) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = new Margin(0, 0, _toolStrip.Bottom, 0), + Visible = false, + Parent = this, + }; + + // Tree host panel + _treePanelRoot = new ContainerControl + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = _split.Panel1, + }; + // Content structure tree searching query input box - var headerPanel = new ContainerControl + _treeHeaderPanel = new ContainerControl { AnchorPreset = AnchorPresets.HorizontalStretchTop, BackgroundColor = style.Background, IsScrollable = false, Offsets = new Margin(0, 0, 0, 18 + 6), + Parent = _treePanelRoot, }; + _foldersSearchBox = new SearchBox { AnchorPreset = AnchorPresets.HorizontalStretchMiddle, - Parent = headerPanel, - Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18), + Parent = _treeHeaderPanel, + Bounds = new Rectangle(4, 4, _treeHeaderPanel.Width - 8, 18), }; _foldersSearchBox.TextChanged += OnFoldersSearchBoxTextChanged; @@ -198,55 +222,74 @@ namespace FlaxEditor.Windows _contentTreePanel = new Panel { AnchorPreset = AnchorPresets.StretchAll, - Offsets = new Margin(0, 0, headerPanel.Bottom, 0), + Offsets = new Margin(0, 0, _treeHeaderPanel.Bottom, 0), IsScrollable = true, ScrollBars = ScrollBars.Both, - Parent = _split.Panel1, + Parent = _treePanelRoot, }; // Content structure tree - _tree = new Tree(false) + _tree = new Tree(true) { DrawRootTreeLine = false, Parent = _contentTreePanel, }; _tree.SelectedChanged += OnTreeSelectionChanged; - headerPanel.Parent = _split.Panel1; // Content items searching query input box and filters selector - var contentItemsSearchPanel = new ContainerControl + _contentItemsSearchPanel = new Panel { AnchorPreset = AnchorPresets.HorizontalStretchTop, IsScrollable = true, Offsets = new Margin(0, 0, 0, 18 + 8), Parent = _split.Panel2, }; - const float viewDropdownWidth = 50.0f; + _itemsSearchBox = new SearchBox { AnchorPreset = AnchorPresets.HorizontalStretchMiddle, - Parent = contentItemsSearchPanel, - Bounds = new Rectangle(viewDropdownWidth + 8, 4, contentItemsSearchPanel.Width - 12 - viewDropdownWidth, 18), + Parent = _contentItemsSearchPanel, + Bounds = new Rectangle(4, 4, _contentItemsSearchPanel.Width - 8, 18), }; _itemsSearchBox.TextChanged += UpdateItemsSearch; + + _viewDropdownPanel = new Panel + { + Width = 50.0f, + Parent = this, + AnchorPreset = AnchorPresets.TopLeft, + BackgroundColor = Color.Transparent, + }; + _viewDropdown = new ViewDropdown { - AnchorPreset = AnchorPresets.MiddleLeft, SupportMultiSelect = true, TooltipText = "Change content view and filter options", - Parent = contentItemsSearchPanel, - Offsets = new Margin(4, viewDropdownWidth, -9, 18), + Offsets = Margin.Zero, + Width = 46.0f, + Height = 18.0f, + Parent = _viewDropdownPanel, }; + _viewDropdown.LocalX += 2.0f; + _viewDropdown.LocalY += _toolStrip.ItemsHeight * 0.5f - 9.0f; _viewDropdown.SelectedIndexChanged += e => UpdateItemsSearch(); for (int i = 0; i <= (int)ContentItemSearchFilter.Other; i++) _viewDropdown.Items.Add(((ContentItemSearchFilter)i).ToString()); _viewDropdown.PopupCreate += OnViewDropdownPopupCreate; + // Navigation bar (after view dropdown so layout order stays stable) + _navigationBar = new NavigationBar + { + Parent = _toolStrip, + ScrollbarTrackColor = style.Background, + ScrollbarThumbColor = style.ForegroundGrey, + }; + // Content view panel _contentViewPanel = new Panel { AnchorPreset = AnchorPresets.StretchAll, - Offsets = new Margin(0, 0, contentItemsSearchPanel.Bottom + 4, 0), + Offsets = new Margin(0, 0, _contentItemsSearchPanel.Bottom + 4, 0), IsScrollable = true, ScrollBars = ScrollBars.Vertical, Parent = _split.Panel2, @@ -266,9 +309,14 @@ namespace FlaxEditor.Windows _view.OnDelete += Delete; _view.OnDuplicate += Duplicate; _view.OnPaste += Paste; + _view.ViewScaleChanged += ApplyTreeViewScale; _view.InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus()); InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus()); + + LoadExpandedFolders(); + UpdateViewDropdownBounds(); + ApplyTreeViewScale(); } private ContextMenu OnViewDropdownPopupCreate(ComboBox comboBox) @@ -287,6 +335,7 @@ namespace FlaxEditor.Windows var viewType = menu.AddChildMenu("View Type"); viewType.ContextMenu.AddButton("Tiles", OnViewTypeButtonClicked).Tag = ContentViewType.Tiles; viewType.ContextMenu.AddButton("List", OnViewTypeButtonClicked).Tag = ContentViewType.List; + viewType.ContextMenu.AddButton("Tree View", OnViewTypeButtonClicked).Tag = "Tree"; viewType.ContextMenu.VisibleChanged += control => { if (!control.Visible) @@ -294,13 +343,23 @@ namespace FlaxEditor.Windows foreach (var item in ((ContextMenu)control).Items) { if (item is ContextMenuButton button) - button.Checked = View.ViewType == (ContentViewType)button.Tag; + { + if (button.Tag is ContentViewType type) + button.Checked = View.ViewType == type && !_showAllContentInTree; + else + button.Checked = _showAllContentInTree; + } } }; var show = menu.AddChildMenu("Show"); { - var b = show.ContextMenu.AddButton("File extensions", () => View.ShowFileExtensions = !View.ShowFileExtensions); + var b = show.ContextMenu.AddButton("File extensions", () => + { + View.ShowFileExtensions = !View.ShowFileExtensions; + if (_showAllContentInTree) + UpdateTreeItemNames(_root); + }); b.TooltipText = "Shows all files with extensions"; b.Checked = View.ShowFileExtensions; b.CloseMenuOnClick = false; @@ -381,9 +440,63 @@ namespace FlaxEditor.Windows RefreshView(); } + private void SetShowAllContentInTree(bool value) + { + if (_showAllContentInTree == value) + return; + + _showAllContentInTree = value; + ApplyTreeViewMode(); + } + + private void ApplyTreeViewMode() + { + if (_treeOnlyPanel == null || _split == null || _treePanelRoot == null) + return; + + if (_showAllContentInTree) + { + _split.Visible = false; + _treeOnlyPanel.Visible = true; + _treePanelRoot.Parent = _treeOnlyPanel; + _treePanelRoot.Offsets = Margin.Zero; + _contentItemsSearchPanel.Visible = false; + _itemsSearchBox.Visible = false; + _contentViewPanel.Visible = false; + RefreshTreeItems(); + } + else + { + _treeOnlyPanel.Visible = false; + _split.Visible = true; + _treePanelRoot.Parent = _split.Panel1; + _treePanelRoot.Offsets = Margin.Zero; + _contentItemsSearchPanel.Visible = true; + _itemsSearchBox.Visible = true; + _contentViewPanel.Visible = true; + if (_tree.SelectedNode is ContentItemTreeNode itemNode && itemNode.Parent is TreeNode parentNode) + _tree.Select(parentNode); + if (_root != null) + RemoveTreeAssetNodes(_root); + RefreshView(SelectedNode); + } + + PerformLayout(); + ApplyTreeViewScale(); + _tree.PerformLayout(); + } + private void OnViewTypeButtonClicked(ContextMenuButton button) { - View.ViewType = (ContentViewType)button.Tag; + if (button.Tag is ContentViewType viewType) + { + SetShowAllContentInTree(false); + View.ViewType = viewType; + } + else + { + SetShowAllContentInTree(true); + } } private void OnFilterClicked(ContextMenuButton filterButton) @@ -443,15 +556,58 @@ namespace FlaxEditor.Windows // Show element in the view Select(item, true); - // Disable scrolling in content view - if (_contentViewPanel.VScrollBar != null) - _contentViewPanel.VScrollBar.ThumbEnabled = false; - if (_contentViewPanel.HScrollBar != null) - _contentViewPanel.HScrollBar.ThumbEnabled = false; - ScrollingOnContentView(false); + // Disable scrolling in proper view + _renameInTree = _showAllContentInTree; + if (_renameInTree) + { + if (_contentTreePanel.VScrollBar != null) + _contentTreePanel.VScrollBar.ThumbEnabled = false; + if (_contentTreePanel.HScrollBar != null) + _contentTreePanel.HScrollBar.ThumbEnabled = false; + ScrollingOnTreeView(false); + } + else + { + if (_contentViewPanel.VScrollBar != null) + _contentViewPanel.VScrollBar.ThumbEnabled = false; + if (_contentViewPanel.HScrollBar != null) + _contentViewPanel.HScrollBar.ThumbEnabled = false; + ScrollingOnContentView(false); + } // Show rename popup - var popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true); + RenamePopup popup; + if (_renameInTree) + { + TreeNode node = null; + if (item is ContentFolder folder) + node = folder.Node; + else if (item.ParentFolder != null) + node = FindTreeItemNode(item.ParentFolder.Node, item); + if (node == null) + { + // Fallback to content view rename + popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true); + } + else + { + var area = node.TextRect; + const float minRenameWidth = 220.0f; + if (area.Width < minRenameWidth) + { + float expand = minRenameWidth - area.Width; + area.X -= expand * 0.5f; + area.Width = minRenameWidth; + } + area.Y -= 2; + area.Height += 4.0f; + popup = RenamePopup.Show(node, area, item.ShortName, true); + } + } + else + { + popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true); + } popup.Tag = item; popup.Validate += OnRenameValidate; popup.Renamed += renamePopup => Rename((ContentItem)renamePopup.Tag, renamePopup.Text); @@ -471,12 +627,24 @@ namespace FlaxEditor.Windows private void OnRenameClosed(RenamePopup popup) { - // Restore scrolling in content view - if (_contentViewPanel.VScrollBar != null) - _contentViewPanel.VScrollBar.ThumbEnabled = true; - if (_contentViewPanel.HScrollBar != null) - _contentViewPanel.HScrollBar.ThumbEnabled = true; - ScrollingOnContentView(true); + // Restore scrolling in proper view + if (_renameInTree) + { + if (_contentTreePanel.VScrollBar != null) + _contentTreePanel.VScrollBar.ThumbEnabled = true; + if (_contentTreePanel.HScrollBar != null) + _contentTreePanel.HScrollBar.ThumbEnabled = true; + ScrollingOnTreeView(true); + } + else + { + if (_contentViewPanel.VScrollBar != null) + _contentViewPanel.VScrollBar.ThumbEnabled = true; + if (_contentViewPanel.HScrollBar != null) + _contentViewPanel.HScrollBar.ThumbEnabled = true; + ScrollingOnContentView(true); + } + _renameInTree = false; // Check if was creating new element if (_newElement != null) @@ -889,6 +1057,16 @@ namespace FlaxEditor.Windows } } + private void OnContentDatabaseItemAdded(ContentItem contentItem) + { + if (contentItem is ContentFolder folder && _expandedFolderPaths.Contains(StringUtils.NormalizePath(folder.Path))) + { + _suppressExpandedStateSave = true; + folder.Node?.Expand(true); + _suppressExpandedStateSave = false; + } + } + /// /// Opens the specified content item. /// @@ -905,7 +1083,8 @@ namespace FlaxEditor.Windows var folder = (ContentFolder)item; folder.Node.Expand(); _tree.Select(folder.Node); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); return; } @@ -946,6 +1125,36 @@ namespace FlaxEditor.Windows // Ensure that window is visible FocusOrShow(); + if (_showAllContentInTree) + { + var targetNode = item is ContentFolder folder ? folder.Node : parent.Node; + if (targetNode != null) + { + targetNode.ExpandAllParents(); + if (item is ContentFolder) + { + _tree.Select(targetNode); + _contentTreePanel.ScrollViewTo(targetNode, fastScroll); + targetNode.Focus(); + } + else + { + var itemNode = FindTreeItemNode(targetNode, item); + if (itemNode != null) + { + _tree.Select(itemNode); + _contentTreePanel.ScrollViewTo(itemNode, fastScroll); + itemNode.Focus(); + } + else + { + _tree.Select(targetNode); + } + } + } + return; + } + // Navigate to the parent directory Navigate(parent.Node); @@ -957,23 +1166,45 @@ namespace FlaxEditor.Windows _view.Focus(); } + private ContentItemTreeNode FindTreeItemNode(ContentFolderTreeNode parentNode, ContentItem item) + { + if (parentNode == null || item == null) + return null; + for (int i = 0; i < parentNode.ChildrenCount; i++) + { + if (parentNode.GetChild(i) is ContentItemTreeNode itemNode && itemNode.Item == item) + return itemNode; + } + return null; + } + /// /// Refreshes the current view items collection. /// public void RefreshView() { - if (_view.IsSearching) + if (_showAllContentInTree) + RefreshTreeItems(); + else if (_view.IsSearching) UpdateItemsSearch(); else RefreshView(SelectedNode); + + return; } /// /// Refreshes the view. /// /// The target location. - public void RefreshView(ContentTreeNode target) + public void RefreshView(ContentFolderTreeNode target) { + if (_showAllContentInTree) + { + RefreshTreeItems(); + return; + } + _view.IsSearching = false; if (target == _root) { @@ -981,7 +1212,7 @@ namespace FlaxEditor.Windows var items = new List(8); for (int i = 0; i < _root.ChildrenCount; i++) { - if (_root.GetChild(i) is ContentTreeNode node) + if (_root.GetChild(i) is ContentFolderTreeNode node) { items.Add(node.Folder); } @@ -1000,12 +1231,262 @@ namespace FlaxEditor.Windows } } + private void RefreshTreeItems() + { + if (!_showAllContentInTree || _root == null) + return; + + _root.LockChildrenRecursive(); + RemoveTreeAssetNodes(_root); + AddTreeAssetNodes(_root); + _root.UnlockChildrenRecursive(); + _tree.PerformLayout(); + } + + private void UpdateTreeItemNames(ContentFolderTreeNode node) + { + if (node == null) + return; + + for (int i = 0; i < node.ChildrenCount; i++) + { + if (node.GetChild(i) is ContentFolderTreeNode childFolder) + { + UpdateTreeItemNames(childFolder); + } + else if (node.GetChild(i) is ContentItemTreeNode itemNode) + { + itemNode.UpdateDisplayedName(); + } + } + } + + internal void OnContentTreeNodeExpandedChanged(ContentFolderTreeNode node, bool isExpanded) + { + if (_suppressExpandedStateSave || node == null || node == _root) + return; + + var path = node.Path; + if (string.IsNullOrEmpty(path)) + return; + path = StringUtils.NormalizePath(path); + + if (isExpanded) + _expandedFolderPaths.Add(path); + else + // Remove all sub paths if parent folder is closed. + _expandedFolderPaths.RemoveWhere(x => x.Contains(path)); + + SaveExpandedFolders(); + } + + internal void TryAutoExpandContentNode(ContentFolderTreeNode node) + { + if (node == null || node == _root) + return; + + var path = node.Path; + if (string.IsNullOrEmpty(path)) + return; + path = StringUtils.NormalizePath(path); + + if (!_expandedFolderPaths.Contains(path)) + return; + + _suppressExpandedStateSave = true; + node.Expand(true); + _suppressExpandedStateSave = false; + } + + private void LoadExpandedFolders() + { + _expandedFolderPaths.Clear(); + if (Editor.ProjectCache.TryGetCustomData(ProjectDataExpandedFolders, out string data) && !string.IsNullOrWhiteSpace(data)) + { + var entries = data.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < entries.Length; i++) + { + var path = entries[i].Trim(); + if (path.Length == 0) + continue; + _expandedFolderPaths.Add(StringUtils.NormalizePath(path)); + } + } + } + + private void SaveExpandedFolders() + { + if (_expandedFolderPaths.Count == 0) + { + Editor.ProjectCache.RemoveCustomData(ProjectDataExpandedFolders); + return; + } + + var data = string.Join("\n", _expandedFolderPaths); + Editor.ProjectCache.SetCustomData(ProjectDataExpandedFolders, data); + } + + private void ApplyExpandedFolders() + { + if (_root == null || _expandedFolderPaths.Count == 0) + return; + + _suppressExpandedStateSave = true; + foreach (var path in _expandedFolderPaths) + { + if (Editor.ContentDatabase.Find(path) is ContentFolder folder) + { + folder.Node.ExpandAllParents(true); + folder.Node.Expand(true); + } + } + _suppressExpandedStateSave = false; + } + + private void RemoveTreeAssetNodes(ContentFolderTreeNode node) + { + for (int i = node.ChildrenCount - 1; i >= 0; i--) + { + if (node.GetChild(i) is ContentItemTreeNode itemNode) + { + node.RemoveChild(itemNode); + itemNode.Dispose(); + } + else if (node.GetChild(i) is ContentFolderTreeNode childFolder) + { + RemoveTreeAssetNodes(childFolder); + } + } + } + + private void AddTreeAssetNodes(ContentFolderTreeNode node) + { + if (node.Folder != null) + { + var children = node.Folder.Children; + for (int i = 0; i < children.Count; i++) + { + var child = children[i]; + if (child is ContentFolder) + continue; + if (!ShouldShowTreeItem(child)) + continue; + + var itemNode = new ContentItemTreeNode(child) + { + Parent = node, + }; + } + } + + for (int i = 0; i < node.ChildrenCount; i++) + { + if (node.GetChild(i) is ContentFolderTreeNode childFolder) + { + AddTreeAssetNodes(childFolder); + } + } + + node.SortChildren(); + } + + private bool ShouldShowTreeItem(ContentItem item) + { + if (item == null || !item.Visible) + return false; + if (_viewDropdown != null && _viewDropdown.HasSelection) + { + var filterIndex = (int)item.SearchFilter; + if (!_viewDropdown.Selection.Contains(filterIndex)) + return false; + } + if (!_showAllFiles && item is FileItem) + return false; + if (!_showGeneratedFiles && IsGeneratedFile(item.Path)) + return false; + return true; + } + + private static bool IsGeneratedFile(string path) + { + return path.EndsWith(".Gen.cs", StringComparison.Ordinal) || + path.EndsWith(".Gen.h", StringComparison.Ordinal) || + path.EndsWith(".Gen.cpp", StringComparison.Ordinal) || + path.EndsWith(".csproj", StringComparison.Ordinal) || + path.Contains(".CSharp"); + } + private void UpdateUI() { UpdateToolstrip(); UpdateNavigationBar(); } + private void ApplyTreeViewScale() + { + if (_tree == null) + return; + + var scale = _showAllContentInTree ? View.ViewScale : 1.0f; + var headerHeight = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var style = Style.Current; + var fontSize = Mathf.Clamp(style.FontSmall.Size * scale, 8.0f, 28.0f); + var fontRef = new FontReference(style.FontSmall.Asset, fontSize); + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var textMarginLeft = 2.0f + Mathf.Max(0.0f, iconSize - 16.0f); + ApplyTreeNodeScale(_root, headerHeight, fontRef, textMarginLeft); + _tree.PerformLayout(); + } + + private void ApplyTreeNodeScale(ContentFolderTreeNode node, float headerHeight, FontReference fontRef, float textMarginLeft) + { + if (node == null) + return; + + var margin = node.TextMargin; + margin.Left = textMarginLeft; + margin.Top = 2.0f; + margin.Right = 2.0f; + margin.Bottom = 2.0f; + node.TextMargin = margin; + node.CustomArrowRect = GetTreeArrowRect(node, headerHeight); + node.HeaderHeight = headerHeight; + node.TextFont = fontRef; + for (int i = 0; i < node.ChildrenCount; i++) + { + if (node.GetChild(i) is ContentFolderTreeNode child) + ApplyTreeNodeScale(child, headerHeight, fontRef, textMarginLeft); + else if (node.GetChild(i) is ContentItemTreeNode itemNode) + { + var itemMargin = itemNode.TextMargin; + itemMargin.Left = textMarginLeft; + itemMargin.Top = 2.0f; + itemMargin.Right = 2.0f; + itemMargin.Bottom = 2.0f; + itemNode.TextMargin = itemMargin; + itemNode.HeaderHeight = headerHeight; + itemNode.TextFont = fontRef; + } + } + } + + private static Rectangle GetTreeArrowRect(ContentFolderTreeNode node, float headerHeight) + { + if (node == null) + return Rectangle.Empty; + + var scale = Editor.Instance?.Windows?.ContentWin?.IsTreeOnlyMode == true + ? Editor.Instance.Windows.ContentWin.View.ViewScale + : 1.0f; + var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f); + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var textRect = node.TextRect; + var iconLeft = textRect.Left - iconSize - 2.0f; + var x = iconLeft - arrowSize - 2.0f; + var y = (headerHeight - arrowSize) * 0.5f; + return new Rectangle(Mathf.Max(x, 0.0f), Mathf.Max(y, 0.0f), arrowSize, arrowSize); + } + private void UpdateToolstrip() { if (_toolStrip == null) @@ -1025,20 +1506,42 @@ namespace FlaxEditor.Windows { var bottomPrev = _toolStrip.Bottom; _navigationBar.UpdateBounds(_toolStrip); + if (_viewDropdownPanel != null && _viewDropdownPanel.Visible) + { + var reserved = _viewDropdownPanel.Width + 8.0f; + _navigationBar.Width = Mathf.Max(_navigationBar.Width - reserved, 0.0f); + } if (bottomPrev != _toolStrip.Bottom) { // Navigation bar changed toolstrip height _split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0); + if (_treeOnlyPanel != null) + _treeOnlyPanel.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0); PerformLayout(); } + UpdateViewDropdownBounds(); } } + private void UpdateViewDropdownBounds() + { + if (_viewDropdownPanel == null || _toolStrip == null) + return; + + var margin = _toolStrip.ItemsMargin; + var height = _toolStrip.ItemsHeight; + var y = _toolStrip.Y + (_toolStrip.Height - height) * 0.5f; + var width = _viewDropdownPanel.Width; + var x = _toolStrip.Right - width - margin.Right; + _viewDropdownPanel.Bounds = new Rectangle(x, y, width, height); + } + /// public override void OnInit() { // Content database events Editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true; + Editor.ContentDatabase.ItemAdded += OnContentDatabaseItemAdded; Editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved; Editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; }; Editor.ContentDatabase.WorkspaceRebuilt += () => @@ -1057,6 +1560,7 @@ namespace FlaxEditor.Windows ShowRoot(); }; + LoadExpandedFolders(); Refresh(); // Load last viewed folder @@ -1072,7 +1576,7 @@ namespace FlaxEditor.Windows private void OnScriptsReloadBegin() { - var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null; + var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentFolderTreeNode : null; _lastViewedFolderBeforeReload = lastViewedFolder?.Path ?? string.Empty; _tree.RemoveChild(_root); @@ -1093,7 +1597,7 @@ namespace FlaxEditor.Windows private void Refresh() { // Setup content root node - _root = new RootContentTreeNode + _root = new RootContentFolderTreeNode { ChildrenIndent = 0 }; @@ -1116,7 +1620,7 @@ namespace FlaxEditor.Windows _root.AddChild(Editor.ContentDatabase.Engine); Editor.ContentDatabase.Game?.Expand(true); - _tree.Margin = new Margin(0.0f, 0.0f, -16.0f, 2.0f); // Hide root node + _tree.Margin = new Margin(0.0f, 0.0f, -16.0f, ScrollBar.DefaultSize + 2); // Hide root node _tree.AddChild(_root); // Setup navigation @@ -1127,6 +1631,8 @@ namespace FlaxEditor.Windows // Update UI layout _isLayoutLocked = false; PerformLayout(); + ApplyExpandedFolders(); + ApplyTreeViewMode(); } /// @@ -1136,7 +1642,10 @@ namespace FlaxEditor.Windows if (_isWorkspaceDirty) { _isWorkspaceDirty = false; - RefreshView(); + if (_showAllContentInTree) + RefreshTreeItems(); + else + RefreshView(); } base.Update(deltaTime); @@ -1146,7 +1655,15 @@ namespace FlaxEditor.Windows public override void OnExit() { // Save last viewed folder - var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null; + ContentFolderTreeNode lastViewedFolder = null; + if (_tree.Selection.Count == 1) + { + var selectedNode = _tree.SelectedNode; + if (selectedNode is ContentItemTreeNode itemNode) + lastViewedFolder = itemNode.Item?.ParentFolder?.Node; + else + lastViewedFolder = selectedNode as ContentFolderTreeNode; + } Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, lastViewedFolder?.Path ?? string.Empty); // Clear view @@ -1157,7 +1674,7 @@ namespace FlaxEditor.Windows { while (_root.HasChildren) { - _root.RemoveChild((ContentTreeNode)_root.GetChild(0)); + _root.RemoveChild((ContentFolderTreeNode)_root.GetChild(0)); } } } @@ -1192,7 +1709,12 @@ namespace FlaxEditor.Windows { ShowContextMenuForItem(null, ref location, false); } - else if (c is ContentTreeNode node) + else if (c is ContentItemTreeNode itemNode) + { + _tree.Select(itemNode); + ShowContextMenuForItem(itemNode.Item, ref location, false); + } + else if (c is ContentFolderTreeNode node) { _tree.Select(node); ShowContextMenuForItem(node.Folder, ref location, true); @@ -1218,11 +1740,16 @@ namespace FlaxEditor.Windows /// protected override void PerformLayoutBeforeChildren() { - UpdateNavigationBarBounds(); - base.PerformLayoutBeforeChildren(); } + /// + protected override void PerformLayoutAfterChildren() + { + base.PerformLayoutAfterChildren(); + UpdateNavigationBarBounds(); + } + /// public override bool UseLayoutData => true; @@ -1237,6 +1764,7 @@ namespace FlaxEditor.Windows writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString()); writer.WriteAttributeString("ShowGeneratedFiles", ShowGeneratedFiles.ToString()); writer.WriteAttributeString("ViewType", _view.ViewType.ToString()); + writer.WriteAttributeString("TreeViewAllContent", _showAllContentInTree.ToString()); } /// @@ -1257,6 +1785,9 @@ namespace FlaxEditor.Windows ShowGeneratedFiles = value2; if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType)) _view.ViewType = viewType; + if (bool.TryParse(node.GetAttribute("TreeViewAllContent"), out value2)) + _showAllContentInTree = value2; + ApplyTreeViewMode(); } /// @@ -1264,6 +1795,7 @@ namespace FlaxEditor.Windows { _split.SplitterValue = 0.2f; _view.ViewScale = 1.0f; + _showAllContentInTree = false; } /// @@ -1272,10 +1804,17 @@ namespace FlaxEditor.Windows _foldersSearchBox = null; _itemsSearchBox = null; _viewDropdown = null; + _viewDropdownPanel = null; + _treePanelRoot = null; + _treeHeaderPanel = null; + _treeOnlyPanel = null; + _contentItemsSearchPanel = null; Editor.Options.OptionsChanged -= OnOptionsChanged; ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; + if (Editor?.ContentDatabase != null) + Editor.ContentDatabase.ItemAdded -= OnContentDatabaseItemAdded; base.OnDestroy(); } From 2c98d805063d0f113975a6e859964dda2dded291 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Mar 2026 23:15:09 +0100 Subject: [PATCH 13/87] fix node auto resizing --- Source/Editor/Surface/Archetypes/Comparisons.cs | 10 +++++----- Source/Editor/Surface/Archetypes/Material.cs | 5 +++-- Source/Editor/Surface/Archetypes/Textures.cs | 7 ++++--- Source/Editor/Surface/Constants.cs | 14 +++++++------- Source/Editor/Surface/ResizableSurfaceNode.cs | 6 ++++++ Source/Editor/Surface/SurfaceComment.cs | 2 +- Source/Editor/Surface/SurfaceNode.cs | 11 +++++++++++ 7 files changed, 37 insertions(+), 18 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index bcf0159d2..da81ba21d 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -39,7 +39,7 @@ namespace FlaxEditor.Surface.Archetypes { NodeElementArchetype.Factory.Input(0, string.Empty, true, null, 0, 0), NodeElementArchetype.Factory.Input(1, string.Empty, true, null, 1, 1), - NodeElementArchetype.Factory.Output(0, title, typeof(bool), 2) + NodeElementArchetype.Factory.Output(0, "Result", typeof(bool), 2) } }; } @@ -173,10 +173,10 @@ namespace FlaxEditor.Surface.Archetypes { Op(1, "==", "Determines whether two values are equal", new[] { "equals" }), Op(2, "!=", "Determines whether two values are not equal", new[] { "not equals" }), - Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than" }), - Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than" }), - Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than" }), - Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than" }), + Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than", "more than" }), + Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than", "tinier than" }), + Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than", "tinier equals than" }), + Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than", "more equals than" }), new NodeArchetype { TypeID = 7, diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 59138c29e..2a9a3ca0d 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -312,11 +312,12 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch) { _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array - Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; + //Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; + Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 25f, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; if (nodeArch.TypeID == 8) { pos += new Float2(60, 0); - size = new Float2(172, 200); + size = new Float2(125, 200); } else { diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index bf43014c0..fe7f3ec39 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -57,7 +57,7 @@ namespace FlaxEditor.Surface.Archetypes { _textureGroupPicker = new ComboBox { - Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level), + Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.LayoutOffsetY * _level), Width = 100, Parent = this, }; @@ -134,7 +134,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, - Size = new Float2(140, 120), + UseFixedSize = true, + Size = new Float2(140, 140), DefaultValues = new object[] { Guid.Empty @@ -493,7 +494,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Lightmap UV", - AlternativeTitles = new string[] { "Lightmap TexCoord" }, + AlternativeTitles = new string[] { "Lightmap TexCoord" }, Description = "Lightmap UVs", Flags = NodeFlags.MaterialGraph, Size = new Float2(110, 20), diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index bec0b5183..7ca322116 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -23,12 +23,12 @@ namespace FlaxEditor.Surface /// /// The node header height. /// - public const float NodeHeaderHeight = 20.0f; + public const float NodeHeaderHeight = 23.0f; /// /// The scale of the header text. /// - public const float NodeHeaderTextScale = 0.65f; + public const float NodeHeaderTextScale = 0.8f; /// /// The node footer height. @@ -38,12 +38,12 @@ namespace FlaxEditor.Surface /// /// The horizontal node margin. /// - public const float NodeMarginX = 8.0f; + public const float NodeMarginX = 6.0f; /// /// The vertical node right margin. /// - public const float NodeMarginY = 7.0f; + public const float NodeMarginY = 8.0f; /// /// The box position offset on the x axis. @@ -58,7 +58,7 @@ namespace FlaxEditor.Surface /// /// The box size (with and height). /// - public const float BoxSize = 13.0f; + public const float BoxSize = 15.0f; /// /// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent. @@ -68,7 +68,7 @@ namespace FlaxEditor.Surface /// /// The offset between the box text and the box /// - public const float BoxTextOffset = 4.0f; + public const float BoxTextOffset = 2.0f; /// /// The width of the rectangle used to draw the box text. @@ -78,6 +78,6 @@ namespace FlaxEditor.Surface /// /// The scale of text of boxes. /// - public const float BoxTextScale = 1.175f; + public const float BoxTextScale = 1f;//1.175f; } } diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 259c29836..ee6560a84 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -74,6 +74,12 @@ namespace FlaxEditor.Surface Resize(size.X, size.Y); } + /// + public override void ResizeAuto() + { + // Do nothing, we want to put full control of node size into the users hands + } + /// public override void Draw() { diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index dea81de15..fb0dd8371 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -56,7 +56,7 @@ namespace FlaxEditor.Surface : base(id, context, nodeArch, groupArch) { _sizeValueIndex = 2; // Index of the Size stored in Values array - _sizeMin = new Float2(140.0f, Constants.NodeHeaderSize); + _sizeMin = new Float2(140.0f, Constants.NodeHeaderHeight); _renameTextBox = new TextBox(false, 0, 0, Width) { Height = Constants.NodeHeaderHeight, diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 56c327f49..1c0728d13 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -938,6 +938,9 @@ namespace FlaxEditor.Surface if (Elements[i] is Box box) box.OnConnectionsChanged(); } + + if (!Archetype.UseFixedSize) + ResizeAuto(); } /// @@ -975,6 +978,9 @@ namespace FlaxEditor.Surface Surface.AddBatchedUndoAction(new EditNodeValuesAction(this, before, graphEdited)); _isDuringValuesEditing = false; + + if (!Archetype.UseFixedSize) + ResizeAuto(); } /// @@ -1009,6 +1015,9 @@ namespace FlaxEditor.Surface } _isDuringValuesEditing = false; + + if (!Archetype.UseFixedSize) + ResizeAuto(); } internal void SetIsDuringValuesEditing(bool value) @@ -1041,6 +1050,8 @@ namespace FlaxEditor.Surface public virtual void ConnectionTick(Box box) { UpdateBoxesTypes(); + if (!Archetype.UseFixedSize) + ResizeAuto(); } /// From 23c3edcab979a875f9cc1f26e045c2f3dd7d5193 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Mar 2026 23:37:33 +0100 Subject: [PATCH 14/87] small cleanup --- Source/Editor/Surface/Archetypes/Comparisons.cs | 2 +- Source/Editor/Surface/Archetypes/Material.cs | 1 - Source/Editor/Surface/Constants.cs | 2 +- Source/Editor/Surface/SurfaceNode.cs | 1 - Source/Editor/Surface/VisjectSurface.Draw.cs | 1 - 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index da81ba21d..32baadc1c 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -39,7 +39,7 @@ namespace FlaxEditor.Surface.Archetypes { NodeElementArchetype.Factory.Input(0, string.Empty, true, null, 0, 0), NodeElementArchetype.Factory.Input(1, string.Empty, true, null, 1, 1), - NodeElementArchetype.Factory.Output(0, "Result", typeof(bool), 2) + NodeElementArchetype.Factory.Output(0, title, typeof(bool), 2) } }; } diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 2a9a3ca0d..241f857b5 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -312,7 +312,6 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch) { _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array - //Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 25f, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; if (nodeArch.TypeID == 8) { diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 7ca322116..2945a85e9 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -78,6 +78,6 @@ namespace FlaxEditor.Surface /// /// The scale of text of boxes. /// - public const float BoxTextScale = 1f;//1.175f; + public const float BoxTextScale = 1f; } } diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 1c0728d13..deca508b1 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -341,7 +341,6 @@ namespace FlaxEditor.Surface if (element is Control control) AddChild(control); - // TODO: Perform this at a better time instead of every time an element gets added. if (!Archetype.UseFixedSize) ResizeAuto(); } diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index 0b7f6eaae..0665a8ebb 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -69,7 +69,6 @@ namespace FlaxEditor.Surface DrawGridBackground(Width, Height); } - // TODO: Rename (and get rid of old texture based draw background?) internal static void DrawBackgroundSolidColor(Color color, float width, float height) { Rectangle backgroundRect = new Rectangle(0f, 0f, width, height); From a9c510c29622aa40baecab50b45e0dbdd5a4adcd Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 5 Mar 2026 19:49:29 +0100 Subject: [PATCH 15/87] tweak some group archetype colors --- Source/Editor/Surface/NodeFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/NodeFactory.cs b/Source/Editor/Surface/NodeFactory.cs index 8448c37a3..30e3628fd 100644 --- a/Source/Editor/Surface/NodeFactory.cs +++ b/Source/Editor/Surface/NodeFactory.cs @@ -116,7 +116,7 @@ namespace FlaxEditor.Surface { GroupID = 9, Name = "Animations", - Color = new Color(105, 179, 160), + Color = new Color(72, 125, 107), Archetypes = Archetypes.Animation.Nodes }, new GroupArchetype @@ -145,7 +145,7 @@ namespace FlaxEditor.Surface { GroupID = 14, Name = "Particles", - Color = new Color(121, 210, 176), + Color = new Color(72, 125, 107), Archetypes = Archetypes.Particles.Nodes }, new GroupArchetype From d3891688f0c6359b4bd61a9552f99bd08354c62f Mon Sep 17 00:00:00 2001 From: Saas Date: Fri, 13 Mar 2026 21:59:53 +0100 Subject: [PATCH 16/87] fix boxes positioning and remove unused constants --- Source/Editor/Surface/Archetypes/Animation.cs | 2 +- Source/Editor/Surface/Constants.cs | 14 ++------------ Source/Editor/Surface/Elements/InputBox.cs | 2 +- Source/Editor/Surface/Elements/OutputBox.cs | 4 ++-- Source/Editor/Surface/NodeElementArchetype.cs | 8 ++++---- Source/Editor/Surface/SurfaceNode.cs | 2 +- Source/Editor/Surface/SurfaceStyle.cs | 2 +- 7 files changed, 12 insertions(+), 22 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index ad5e45611..48a54a56f 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -243,7 +243,7 @@ namespace FlaxEditor.Surface.Archetypes { Type = NodeElementType.Input, Position = new Float2( - FlaxEditor.Surface.Constants.NodeMarginX - FlaxEditor.Surface.Constants.BoxOffsetX, + FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + ylevel * FlaxEditor.Surface.Constants.LayoutOffsetY), Text = "Pose " + _blendPoses.Count, Single = true, diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 2945a85e9..9b857805e 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -23,7 +23,7 @@ namespace FlaxEditor.Surface /// /// The node header height. /// - public const float NodeHeaderHeight = 23.0f; + public const float NodeHeaderHeight = 25.0f; /// /// The scale of the header text. @@ -45,11 +45,6 @@ namespace FlaxEditor.Surface /// public const float NodeMarginY = 8.0f; - /// - /// The box position offset on the x axis. - /// - public const float BoxOffsetX = 0.0f; - /// /// The width of the row that is started by a box. /// @@ -68,16 +63,11 @@ namespace FlaxEditor.Surface /// /// The offset between the box text and the box /// - public const float BoxTextOffset = 2.0f; + public const float BoxTextOffset = 1.65f; /// /// The width of the rectangle used to draw the box text. /// public const float BoxTextRectWidth = 500.0f; - - /// - /// The scale of text of boxes. - /// - public const float BoxTextScale = 1f; } } diff --git a/Source/Editor/Surface/Elements/InputBox.cs b/Source/Editor/Surface/Elements/InputBox.cs index 5a4bfc31c..3e2bb4d5c 100644 --- a/Source/Editor/Surface/Elements/InputBox.cs +++ b/Source/Editor/Surface/Elements/InputBox.cs @@ -1443,7 +1443,7 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; var rect = new Rectangle(Width + Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); - Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, Constants.BoxTextScale); + Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); } /// diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index 3e473d7ac..fa9d15c22 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -234,8 +234,8 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; - var rect = new Rectangle(-Constants.BoxTextRectWidth - Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); - Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center, TextWrapping.NoWrap, 1f, Constants.BoxTextScale); + var rect = new Rectangle(-Constants.BoxTextRectWidth - Constants.BoxTextOffset * 2f, 0f, Constants.BoxTextRectWidth, Height); + Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center); } } } diff --git a/Source/Editor/Surface/NodeElementArchetype.cs b/Source/Editor/Surface/NodeElementArchetype.cs index d8008f53f..55c8a43a7 100644 --- a/Source/Editor/Surface/NodeElementArchetype.cs +++ b/Source/Editor/Surface/NodeElementArchetype.cs @@ -106,7 +106,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Input, Position = new Float2( - Constants.NodeMarginX - Constants.BoxOffsetX, + Constants.NodeMarginX, Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, @@ -132,7 +132,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Input, Position = new Float2( - Constants.NodeMarginX - Constants.BoxOffsetX, + Constants.NodeMarginX, Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, @@ -157,7 +157,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxRowHeight + Constants.BoxOffsetX, + -Constants.NodeMarginX, Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, @@ -182,7 +182,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxSize + Constants.BoxOffsetX, + -Constants.NodeMarginX, Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index deca508b1..a775d446c 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -199,7 +199,7 @@ namespace FlaxEditor.Surface { if (Elements[i] is OutputBox box) { - box.Location = box.Archetype.Position + new Float2(width, 0); + box.Location = box.Archetype.Position + new Float2(width - Constants.NodeMarginX, 0); } } diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index 8d547c14e..94d9b04df 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -222,7 +222,7 @@ namespace FlaxEditor.Surface private static void DefaultDrawBox(Elements.Box box) { - var rect = new Rectangle(0.0f, box.Height * 0.5f - Constants.BoxSize * 0.5f, new Float2(Constants.BoxSize)); + var rect = new Rectangle(box.Width * 0.5f - Constants.BoxSize * 0.5f, box.Height * 0.5f - Constants.BoxSize * 0.5f, new Float2(Constants.BoxSize)); // Size culling const float minBoxSize = 5.0f; From e008c7e2b4d689bee4e7534628d52e9c2a81718c Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 14:20:00 +0100 Subject: [PATCH 17/87] fix behavior tree nodes --- .../Editor/Surface/Archetypes/BehaviorTree.cs | 78 ++++++++++++++++--- Source/Editor/Surface/SurfaceNode.cs | 4 +- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 63f6f5c2d..69a6e8f7e 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -189,21 +189,74 @@ namespace FlaxEditor.Surface.Archetypes public override void Draw() { - base.Draw(); + var style = Style.Current; + + var backgroundRect = new Rectangle(Float2.Zero, Size); + + // Shadow + if (DrawBasicShadow) + { + var shadowRect = backgroundRect.MakeOffsetted(ShadowOffset); + Render2D.FillRectangle(shadowRect, Color.Black.AlphaMultiplied(0.125f)); + } + + // Background + Render2D.FillRectangle(backgroundRect, ArchetypeColor); + + // Breakpoint hit + if (Breakpoint.Hit) + { + var colorTop = Color.OrangeRed; + var colorBottom = Color.Red; + var time = DateTime.Now - Engine.StartupTime; + Render2D.DrawRectangle(backgroundRect.MakeExpanded(Mathf.Lerp(3.0f, 12.0f, Mathf.Sin((float)time.TotalSeconds * 10.0f) * 0.5f + 0.5f)), colorTop, colorTop, colorBottom, colorBottom, 2.0f); + } + + // Header + var headerColor = style.BackgroundHighlighted; + if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting) + headerColor *= 1.07f; + Render2D.FillRectangle(_headerRect, style.BackgroundHighlighted); + Render2D.DrawText(style.FontLarge, Title, _headerTextRect, style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, FlaxEditor.Surface.Constants.NodeHeaderTextScale); + + // Close button + if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) + { + bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting; + Render2D.DrawSprite(style.Cross, _closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); + } + + DrawChildren(); + + // Selection outline + if (_isSelected) + { + var colorTop = Color.Orange; + var colorBottom = Color.OrangeRed; + Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom, 2.5f); + } + + // Breakpoint dot + if (Breakpoint.Set) + { + var icon = Breakpoint.Enabled ? Surface.Style.Icons.BoxClose : Surface.Style.Icons.BoxOpen; + Render2D.DrawSprite(icon, new Rectangle(-7, -7, 16, 16), new Color(0.9f, 0.9f, 0.9f)); + Render2D.DrawSprite(icon, new Rectangle(-6, -6, 14, 14), new Color(0.894117647f, 0.0784313725f, 0.0f)); + } + + if (highlightBox != null) + Render2D.DrawRectangle(highlightBox.Bounds, style.BorderHighlighted, 2f); // Debug Info if (!string.IsNullOrEmpty(_debugInfo)) - { - var style = Style.Current; Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 4, _debugInfoSize), style.Foreground); - } // Debug relevancy outline if (_debugRelevant) { var colorTop = Color.LightYellow; var colorBottom = Color.Yellow; - var backgroundRect = new Rectangle(Float2.One, Size - new Float2(2.0f)); + backgroundRect = new Rectangle(Float2.One, Size - new Float2(2.0f)); Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom); } } @@ -515,7 +568,7 @@ namespace FlaxEditor.Surface.Archetypes height += decorator.Height + DecoratorsMarginY; width = Mathf.Max(width, decorator.Width - FlaxEditor.Surface.Constants.NodeCloseButtonSize - 2 * DecoratorsMarginX); } - Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.NodeFooterSize); + Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderHeight); UpdateRectangles(); } @@ -536,13 +589,13 @@ namespace FlaxEditor.Surface.Archetypes if (decorator.IndexInParent < indexInParent) decorator.IndexInParent = indexInParent + 1; // Push elements above the node } - const float footerSize = FlaxEditor.Surface.Constants.NodeFooterSize; - const float headerSize = FlaxEditor.Surface.Constants.NodeHeaderHeight; + + const float headerHeight = FlaxEditor.Surface.Constants.NodeHeaderHeight; const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; - _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerSize); + float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.65f; + _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerHeight); + _headerTextRect = _headerRect with { X = 5f, Width = Width - closeButtonSize - closeButtonMargin * 4f }; _closeButtonRect = new Rectangle(bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); - _footerRect = new Rectangle(0, bounds.Height - footerSize, bounds.Width, footerSize); if (_output != null && _output.Visible) { _footerRect.Y -= ConnectionAreaHeight; @@ -684,6 +737,9 @@ namespace FlaxEditor.Surface.Archetypes base.UpdateRectangles(); _footerRect = Rectangle.Empty; + const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; + float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.65f; + _closeButtonRect = new Rectangle(Bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); if (_dragIcon != null) _dragIcon.Bounds = new Rectangle(_closeButtonRect.X - _closeButtonRect.Width, _closeButtonRect.Y, _closeButtonRect.Size); } diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index a775d446c..0ee35e23e 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -159,7 +159,7 @@ namespace FlaxEditor.Surface public virtual string ContentSearchText => null; /// - /// Gets the color of the footer of the node. + /// Gets the color of the header of the node. /// protected virtual Color ArchetypeColor => GroupArchetype.Color; @@ -1061,7 +1061,7 @@ namespace FlaxEditor.Surface const float closeButtonMargin = Constants.NodeCloseButtonMargin; const float closeButtonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); - _headerTextRect = _headerRect with { Width = _headerRect.Width - 5f, X = _headerRect.X + 5f }; + _headerTextRect = _headerRect with { X = 5f, Width = Width - closeButtonSize - closeButtonMargin * 4f }; _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); _footerRect = new Rectangle(0, Height - footerSize, Width, footerSize); } From fcdd05dede36087ec603e386b5d0b71dfc108937 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 14:20:30 +0100 Subject: [PATCH 18/87] fix straight connection sprite rotation offset at some angles --- Source/Editor/Surface/SurfaceStyle.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index 94d9b04df..d5bbdaf62 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -329,13 +329,11 @@ namespace FlaxEditor.Surface { var dir = sub / length; var arrowRect = new Rectangle(0, 0, 16.0f, 16.0f); - float rotation = Float2.Dot(dir, Float2.UnitY); - if (endPos.X < startPos.X) - rotation = 2 - rotation; + float rotation = Mathf.Atan2(dir.Y, dir.X); var sprite = Editor.Instance.Icons.VisjectArrowClosed32; var arrowTransform = Matrix3x3.Translation2D(-6.5f, -8) * - Matrix3x3.RotationZ(rotation * Mathf.PiOverTwo) * + Matrix3x3.RotationZ(rotation) * Matrix3x3.Translation2D(endPos - dir * 8); Render2D.PushTransform(ref arrowTransform); From 0cec65f35f309ddd194783291e02d3f41c047873 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 14:46:10 +0100 Subject: [PATCH 19/87] disable vertex snapping while drawing box to fix snapping artefacts when zooming surface --- Source/Editor/Surface/SurfaceStyle.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index d5bbdaf62..0bfdd641e 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -244,6 +244,10 @@ namespace FlaxEditor.Surface else icon = hasConnections ? style.Icons.BoxClose : style.Icons.BoxOpen; color *= box.ConnectionsHighlightIntensity + 1; + + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + Render2D.DrawSprite(icon, rect, color); // Draw connected hint with color from connected output box @@ -264,6 +268,8 @@ namespace FlaxEditor.Surface Color selectionColor = FlaxEngine.GUI.Style.Current.BorderSelected.RGBMultiplied(1.0f + outlineAlpha * 0.4f); Render2D.DrawSprite(icon, outlineRect, selectionColor.AlphaMultiplied(0.4f)); } + + Render2D.Features = features; } /// From 2fbeac60774bf87ae97b87f2eff22d37f2b64bfc Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 16:29:43 +0100 Subject: [PATCH 20/87] disable decorator node shadows --- Source/Editor/Surface/Archetypes/BehaviorTree.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 69a6e8f7e..7eb9df286 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -701,6 +701,8 @@ namespace FlaxEditor.Surface.Archetypes private DragDecorator _dragDecorator; private float _dragLocation = -1; + internal override bool DrawBasicShadow => false; + internal Decorator(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { From 60076d48c761e4b661015d1c2067177acd8caaff Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 16:38:56 +0100 Subject: [PATCH 21/87] fix box alignment for nodes with fixed size and adjust some node sizes --- Source/Editor/Surface/Archetypes/Constants.cs | 18 ++++++++++++------ Source/Editor/Surface/Archetypes/Textures.cs | 3 ++- Source/Editor/Surface/NodeArchetype.cs | 3 +++ Source/Editor/Surface/SurfaceNode.cs | 10 ++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index 680cc512d..562cd98e0 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -482,7 +482,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + UseFixedSize = true, + Size = new Float2(90, 20), DefaultValues = new object[] { false @@ -515,7 +516,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + UseFixedSize = true, + Size = new Float2(120, 20), DefaultValues = new object[] { 0 @@ -543,7 +545,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + UseFixedSize = true, + Size = new Float2(120, 20), DefaultValues = new object[] { 0.0f @@ -750,7 +753,8 @@ namespace FlaxEditor.Surface.Archetypes Title = "PI", Description = "A value specifying the approximation of Ï€ which is 180 degrees", Flags = NodeFlags.AllGraphs, - Size = new Float2(50, 20), + UseFixedSize = true, + Size = new Float2(45, 20), Elements = new[] { NodeElementArchetype.Factory.Output(0, "Ï€", typeof(float), 0), @@ -782,7 +786,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, - Size = new Float2(170, 20), + UseFixedSize = true, + Size = new Float2(130, 20), DefaultValues = new object[] { 0u @@ -824,7 +829,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + UseFixedSize = true, + Size = new Float2(120, 20), DefaultValues = new object[] { 0.0d diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index fe7f3ec39..c7d0d3b5b 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -159,7 +159,8 @@ namespace FlaxEditor.Surface.Archetypes AlternativeTitles = new string[] { "UV", "UVs" }, Description = "Texture coordinates", Flags = NodeFlags.MaterialGraph, - Size = new Float2(150, 30), + UseFixedSize = true, + Size = new Float2(160, 20), DefaultValues = new object[] { 0u diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 18e6ac8c9..006bd2fb6 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -129,6 +129,9 @@ namespace FlaxEditor.Surface /// public NodeFlags Flags; + /// + /// If the node should use the as node size. If false, the node will auto resize based on its elements. + /// public bool UseFixedSize = false; /// diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 0ee35e23e..9a1fd2ef4 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -343,6 +343,8 @@ namespace FlaxEditor.Surface if (!Archetype.UseFixedSize) ResizeAuto(); + else + Resize(Archetype.Size.X, Archetype.Size.Y); } /// @@ -940,6 +942,8 @@ namespace FlaxEditor.Surface if (!Archetype.UseFixedSize) ResizeAuto(); + else + Resize(Archetype.Size.X, Archetype.Size.Y); } /// @@ -980,6 +984,8 @@ namespace FlaxEditor.Surface if (!Archetype.UseFixedSize) ResizeAuto(); + else + Resize(Archetype.Size.X, Archetype.Size.Y); } /// @@ -1017,6 +1023,8 @@ namespace FlaxEditor.Surface if (!Archetype.UseFixedSize) ResizeAuto(); + else + Resize(Archetype.Size.X, Archetype.Size.Y); } internal void SetIsDuringValuesEditing(bool value) @@ -1051,6 +1059,8 @@ namespace FlaxEditor.Surface UpdateBoxesTypes(); if (!Archetype.UseFixedSize) ResizeAuto(); + else + Resize(Archetype.Size.X, Archetype.Size.Y); } /// From 44ecac7ffc34d0102f142eccc3ad49c074d64fad Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 16:52:56 +0100 Subject: [PATCH 22/87] make debug text more legible --- Source/Editor/Surface/Archetypes/BehaviorTree.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 7eb9df286..30b05ed78 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -249,7 +249,11 @@ namespace FlaxEditor.Surface.Archetypes // Debug Info if (!string.IsNullOrEmpty(_debugInfo)) - Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 4, _debugInfoSize), style.Foreground); + { + // Draw an extra background to cover the archetype color colored node background and make text more legible + Render2D.FillRectangle(new Rectangle(0, _headerRect.Bottom + 4, Width, Height - _headerRect.Bottom - 4), style.BackgroundHighlighted); + Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 7, _debugInfoSize), style.Foreground, scale: 0.8f); + } // Debug relevancy outline if (_debugRelevant) From f2a13b64d489830b5c426e0d12a899dc54920b5c Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 19:00:12 +0100 Subject: [PATCH 23/87] fix reroute node output box moving on connection change --- Source/Editor/Surface/Archetypes/Tools.cs | 6 ++++++ Source/Editor/Surface/SurfaceNode.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index cc6820020..bd5818693 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1179,6 +1179,12 @@ namespace FlaxEditor.Surface.Archetypes _footerRect = Rectangle.Empty; } + /// + public override void Resize(float width, float height) + { + // Do nothing so the input and output boxes do not change position + } + /// public override void Draw() { diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 9a1fd2ef4..9fb6dfbbd 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -181,7 +181,7 @@ namespace FlaxEditor.Surface /// /// The width. /// The height. - public void Resize(float width, float height) + public virtual void Resize(float width, float height) { if (Surface == null) return; From 5ec018b904989bcfae6d2ec8cc51a4b38f9798ab Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 14 Mar 2026 19:05:35 +0100 Subject: [PATCH 24/87] fix particle module changing size after certain user actions --- Source/Editor/Surface/Archetypes/ParticleModules.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/ParticleModules.cs b/Source/Editor/Surface/Archetypes/ParticleModules.cs index debc88d59..f65c54646 100644 --- a/Source/Editor/Surface/Archetypes/ParticleModules.cs +++ b/Source/Editor/Surface/Archetypes/ParticleModules.cs @@ -138,6 +138,12 @@ namespace FlaxEditor.Surface.Archetypes } } + /// + public override void Resize(float width, float height) + { + // Do nothing so module does not change size + } + private bool ArrangeAreaCheck(out int index, out Rectangle rect) { var barSidesExtend = 20.0f; From 0237235dcbdd42347f240e96ed23e9852ed886bb Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 15 Mar 2026 14:29:31 +0100 Subject: [PATCH 25/87] draw close button using Render2D to make it look good even when zoomed in a lot --- .../Editor/Surface/Archetypes/BehaviorTree.cs | 11 ++++++---- .../Surface/Archetypes/ParticleModules.cs | 6 ++--- Source/Editor/Surface/Constants.cs | 4 ++-- Source/Editor/Surface/SurfaceComment.cs | 4 ++-- Source/Editor/Surface/SurfaceNode.cs | 22 +++++++++++++++++-- Source/Editor/Surface/SurfaceStyle.cs | 3 ++- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 30b05ed78..c112bcf27 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -223,7 +223,7 @@ namespace FlaxEditor.Surface.Archetypes if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) { bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting; - Render2D.DrawSprite(style.Cross, _closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); } DrawChildren(); @@ -596,7 +596,7 @@ namespace FlaxEditor.Surface.Archetypes const float headerHeight = FlaxEditor.Surface.Constants.NodeHeaderHeight; const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.65f; + float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.75f; _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerHeight); _headerTextRect = _headerRect with { X = 5f, Width = Width - closeButtonSize - closeButtonMargin * 4f }; _closeButtonRect = new Rectangle(bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); @@ -744,10 +744,13 @@ namespace FlaxEditor.Surface.Archetypes _footerRect = Rectangle.Empty; const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.65f; + float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.75f; _closeButtonRect = new Rectangle(Bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); if (_dragIcon != null) - _dragIcon.Bounds = new Rectangle(_closeButtonRect.X - _closeButtonRect.Width, _closeButtonRect.Y, _closeButtonRect.Size); + { + var dragIconRect = _closeButtonRect.MakeExpanded(5f); + _dragIcon.Bounds = new Rectangle(dragIconRect.X - dragIconRect.Width, dragIconRect.Y, dragIconRect.Size); + } } protected override void UpdateTitle() diff --git a/Source/Editor/Surface/Archetypes/ParticleModules.cs b/Source/Editor/Surface/Archetypes/ParticleModules.cs index f65c54646..e85c87898 100644 --- a/Source/Editor/Surface/Archetypes/ParticleModules.cs +++ b/Source/Editor/Surface/Archetypes/ParticleModules.cs @@ -121,7 +121,7 @@ namespace FlaxEditor.Surface.Archetypes Render2D.DrawRectangle(new Rectangle(1, 0, Width - 2, Height - 1), Colors[idx]); // Close button - Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); // Arrange button var dragBarColor = _arrangeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey; @@ -267,9 +267,9 @@ namespace FlaxEditor.Surface.Archetypes const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); - _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); + _closeButtonRect = new Rectangle(Width - closeButtonSize * 0.75f - closeButtonMargin, closeButtonMargin + 0.25f, closeButtonSize * 0.75f, closeButtonSize * 0.75f); _footerRect = Rectangle.Empty; - _enabled.Location = new Float2(_closeButtonRect.X - _enabled.Width - 2, _closeButtonRect.Y); + _enabled.Location = new Float2(_closeButtonRect.X - _enabled.Width - 2, _closeButtonRect.Y - 0.25f); _arrangeButtonRect = new Rectangle(_enabled.X - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); } diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 9b857805e..474d3979b 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -13,12 +13,12 @@ namespace FlaxEditor.Surface /// /// The node close button size. /// - public const float NodeCloseButtonSize = 16.0f; + public const float NodeCloseButtonSize = 10.0f; /// /// The node close button margin from the edges. /// - public const float NodeCloseButtonMargin = 2.0f; + public const float NodeCloseButtonMargin = 5.0f; /// /// The node header height. diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index fb0dd8371..79a285bd8 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -128,7 +128,7 @@ namespace FlaxEditor.Surface const float buttonMargin = Constants.NodeCloseButtonMargin; const float buttonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); - _closeButtonRect = new Rectangle(Width - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); + _closeButtonRect = new Rectangle(Width - buttonSize * 0.75f - buttonMargin, buttonMargin, buttonSize * 0.75f, buttonSize * 0.75f); _colorButtonRect = new Rectangle(_closeButtonRect.Left - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin, buttonSize, buttonSize); _renameTextBox.Width = Width; @@ -183,7 +183,7 @@ namespace FlaxEditor.Surface if (Surface.CanEdit) { // Close button - Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); // Color button Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 9fb6dfbbd..2b053b315 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -454,7 +454,7 @@ namespace FlaxEditor.Surface private static readonly List UpdateStack = new List(); /// - /// Updates dependant/independent boxes types. + /// Updates dependent/independent boxes types. /// public void UpdateBoxesTypes() { @@ -796,6 +796,24 @@ namespace FlaxEditor.Surface return output; } + /// + /// Draws the close button inside of the . + /// + /// The rectangle to draw the close button in. + /// The color of the close button. + public void DrawCloseButton(Rectangle rect, Color color) + { + // Disable vertex snapping to reduce artefacts at the line ends + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + + rect.Expand(-2f); // Don't overshoot the rectangle because of the thickness + Render2D.DrawLine(rect.TopLeft, rect.BottomRight, color, 2f); + Render2D.DrawLine(rect.BottomLeft, rect.TopRight, color, 2f); + + Render2D.Features = features; + } + /// /// Draws all the connections between surface objects related to this node. /// @@ -1113,7 +1131,7 @@ namespace FlaxEditor.Surface if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) { bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting; - Render2D.DrawSprite(style.Cross, _closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); } // Footer diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index 0bfdd641e..4fc656823 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -245,6 +245,7 @@ namespace FlaxEditor.Surface icon = hasConnections ? style.Icons.BoxClose : style.Icons.BoxOpen; color *= box.ConnectionsHighlightIntensity + 1; + // Disable vertex snapping to prevent position jitter/ snapping artefacts for the boxes when zooming the surface var features = Render2D.Features; Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; @@ -273,7 +274,7 @@ namespace FlaxEditor.Surface } /// - /// Function used to create style for the given surface type. Can be overriden to provide some customization via user plugin. + /// Function used to create style for the given surface type. Can be overridden to provide some customization via user plugin. /// public static Func CreateStyleHandler = CreateDefault; From 5655cc8f422c58de7234e4aa715d294f606475e5 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 15 Mar 2026 16:59:09 +0100 Subject: [PATCH 26/87] account for surface node elements when auto resizing --- Source/Editor/Surface/Archetypes/Constants.cs | 6 ------ Source/Editor/Surface/Archetypes/Textures.cs | 2 -- Source/Editor/Surface/NodeElementArchetype.cs | 9 ++++++++- Source/Editor/Surface/SurfaceNode.cs | 11 +++++++++++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index 562cd98e0..a11213f93 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -482,7 +482,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(90, 20), DefaultValues = new object[] { @@ -516,7 +515,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(120, 20), DefaultValues = new object[] { @@ -545,7 +543,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(120, 20), DefaultValues = new object[] { @@ -753,7 +750,6 @@ namespace FlaxEditor.Surface.Archetypes Title = "PI", Description = "A value specifying the approximation of Ï€ which is 180 degrees", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(45, 20), Elements = new[] { @@ -786,7 +782,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(130, 20), DefaultValues = new object[] { @@ -829,7 +824,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(120, 20), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index c7d0d3b5b..2cc56c8a4 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -134,7 +134,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, - UseFixedSize = true, Size = new Float2(140, 140), DefaultValues = new object[] { @@ -159,7 +158,6 @@ namespace FlaxEditor.Surface.Archetypes AlternativeTitles = new string[] { "UV", "UVs" }, Description = "Texture coordinates", Flags = NodeFlags.MaterialGraph, - UseFixedSize = true, Size = new Float2(160, 20), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/NodeElementArchetype.cs b/Source/Editor/Surface/NodeElementArchetype.cs index 55c8a43a7..0e3c504ff 100644 --- a/Source/Editor/Surface/NodeElementArchetype.cs +++ b/Source/Editor/Surface/NodeElementArchetype.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; using System.Reflection; -using FlaxEditor.CustomEditors; +using FlaxEditor.GUI.Input; using FlaxEditor.Scripting; +using FlaxEditor.Surface.Elements; using FlaxEngine; namespace FlaxEditor.Surface @@ -205,6 +206,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.BoolValue, Position = new Float2(x, y), + Size = new Float2(16f), Text = null, Single = false, ValueIndex = valueIndex, @@ -229,6 +231,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.IntegerValue, Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(50f, IntegerValue.DefaultHeight), Text = null, Single = false, ValueIndex = valueIndex, @@ -255,6 +258,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.UnsignedIntegerValue, Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(50f, UnsignedIntegerValue.DefaultHeight), Text = null, Single = false, ValueIndex = valueIndex, @@ -281,6 +285,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.FloatValue, Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(50f, FloatValueBox.DefaultHeight), Text = null, Single = false, ValueIndex = valueIndex, @@ -360,6 +365,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.ColorValue, Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(32, 18), Text = null, Single = false, ValueIndex = valueIndex, @@ -382,6 +388,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Asset, Position = new Float2(x, y), + Size = new Float2(78f, 90f), Text = type.FullName, Single = false, ValueIndex = valueIndex, diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 2b053b315..af62c8318 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -242,6 +242,17 @@ namespace FlaxEditor.Surface rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 20); rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + 20.0f); } + // Elements (Float-, int-, uint- value boxes, asset pickers, etc.) + else if (child is ISurfaceNodeElement surfaceElement) + { + leftWidth = Mathf.Max(leftWidth, surfaceElement.Archetype.Size.X + 8f); + leftHeight = Mathf.Max(leftHeight, surfaceElement.Archetype.Size.Y + 8f); + } + else if (child is SurfaceNodeElementControl elementControl) + { + leftWidth = Mathf.Max(leftWidth, elementControl.Width + 8f); + leftHeight = Mathf.Max(leftHeight, elementControl.Height + 8f); + } // Other controls in the node else if (child is Control control) { From 5b2b1930d26019fcb6f7d35cb24bbd5adf4f9bc0 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 15 Mar 2026 17:03:01 +0100 Subject: [PATCH 27/87] fix Particle Emitter node to not auto resize --- Source/Editor/Surface/Archetypes/Particles.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs index 5346972e9..d5848a8e4 100644 --- a/Source/Editor/Surface/Archetypes/Particles.cs +++ b/Source/Editor/Surface/Archetypes/Particles.cs @@ -341,6 +341,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Particle Emitter", Description = "Main particle emitter node. Contains a set of modules per emitter context. Modules are executed in order from top to bottom of the stack.", Flags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoRemove | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste | NodeFlags.NoCloseButton, + UseFixedSize = true, Size = new Float2(300, 600), DefaultValues = new object[] { From d5d10aa3293b8c5138c1af2c3ec4ee89debc40d9 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 15 Mar 2026 19:53:09 +0100 Subject: [PATCH 28/87] fix handling controls during resizing --- Source/Editor/Surface/Archetypes/Tools.cs | 1 + Source/Editor/Surface/SurfaceNode.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index bd5818693..5fa66a3ea 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1727,6 +1727,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new AsNode(id, context, arch, groupArch), Description = "Casts the object to a different type. Returns null if cast fails.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, + //UseFixedSize = true, Size = new Float2(200, 20), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index af62c8318..a88260629 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -243,6 +243,7 @@ namespace FlaxEditor.Surface rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + 20.0f); } // Elements (Float-, int-, uint- value boxes, asset pickers, etc.) + // These will only ever be on the left side of the node, so we only adjust left width and height else if (child is ISurfaceNodeElement surfaceElement) { leftWidth = Mathf.Max(leftWidth, surfaceElement.Archetype.Size.X + 8f); @@ -258,12 +259,12 @@ namespace FlaxEditor.Surface { if (control.AnchorPreset == AnchorPresets.TopLeft) { - width = Mathf.Max(width, control.Right + 4 - Constants.NodeMarginX); + width = Mathf.Max(width, control.Right + 15 + Constants.NodeMarginX); height = Mathf.Max(height, control.Bottom + 4 - Constants.NodeMarginY - Constants.NodeHeaderHeight); } else if (!_headerRect.Intersects(control.Bounds)) { - width = Mathf.Max(width, control.Width + 4); + width = Mathf.Max(width, control.Width + 15 + Constants.NodeMarginX); height = Mathf.Max(height, control.Height + 4); } } From a1af870874f5d3f35477e0eebb9c1daec3ddeec4 Mon Sep 17 00:00:00 2001 From: Saas Date: Fri, 20 Mar 2026 19:01:52 +0100 Subject: [PATCH 29/87] fix surface elements resizing the node --- Source/Editor/Surface/SurfaceNode.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index a88260629..b37d85d3c 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -244,11 +244,6 @@ namespace FlaxEditor.Surface } // Elements (Float-, int-, uint- value boxes, asset pickers, etc.) // These will only ever be on the left side of the node, so we only adjust left width and height - else if (child is ISurfaceNodeElement surfaceElement) - { - leftWidth = Mathf.Max(leftWidth, surfaceElement.Archetype.Size.X + 8f); - leftHeight = Mathf.Max(leftHeight, surfaceElement.Archetype.Size.Y + 8f); - } else if (child is SurfaceNodeElementControl elementControl) { leftWidth = Mathf.Max(leftWidth, elementControl.Width + 8f); From 4594a8475345cc1c63c1289dabfa8435c125efbf Mon Sep 17 00:00:00 2001 From: Saas Date: Fri, 20 Mar 2026 19:07:11 +0100 Subject: [PATCH 30/87] disable vertex snapping for Blend Points of Animation Blend 1D and 2D --- Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index 450960af7..1d842a7c3 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -102,8 +102,14 @@ namespace FlaxEditor.Surface.Archetypes outline = style.BorderHighlighted; else if (_editor._node.SelectedAnimationIndex == _index) outline = style.BackgroundSelected; + + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + Render2D.DrawSprite(icon, rect.MakeExpanded(4.0f), outline); Render2D.DrawSprite(icon, rect, style.Foreground); + + Render2D.Features = features; } /// From 1bd86ca28b4b002c6b14ca6de382621f86b72d58 Mon Sep 17 00:00:00 2001 From: Saas Date: Fri, 20 Mar 2026 20:17:10 +0100 Subject: [PATCH 31/87] temporary fix for "Copy Node" node size --- Source/Editor/Surface/Archetypes/Animation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index 48a54a56f..a3fc66b82 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -932,6 +932,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Copy Node", Description = "Copies the skeleton node transformation data (in local space)", Flags = NodeFlags.AnimGraph, + UseFixedSize = true, Size = new Float2(260, 140), DefaultValues = new object[] { From c673e9f52db3d767ef46d85370b2c212f5190896 Mon Sep 17 00:00:00 2001 From: Saas Date: Fri, 20 Mar 2026 20:18:12 +0100 Subject: [PATCH 32/87] minor fixes --- Source/Editor/Surface/Archetypes/Tools.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 5fa66a3ea..bd5818693 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1727,7 +1727,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new AsNode(id, context, arch, groupArch), Description = "Casts the object to a different type. Returns null if cast fails.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, - //UseFixedSize = true, Size = new Float2(200, 20), DefaultValues = new object[] { From a346e258d3c75ae714cadacc4b0e37b63faa8cf5 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 21 Mar 2026 12:53:10 +0100 Subject: [PATCH 33/87] make "Flow" group archetype color more legible --- Source/Editor/Surface/NodeFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Surface/NodeFactory.cs b/Source/Editor/Surface/NodeFactory.cs index 30e3628fd..f787899dc 100644 --- a/Source/Editor/Surface/NodeFactory.cs +++ b/Source/Editor/Surface/NodeFactory.cs @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface { GroupID = 17, Name = "Flow", - Color = new Color(237, 136, 64), + Color = new Color(181, 91, 33), Archetypes = Archetypes.Flow.Nodes }, new GroupArchetype From c71e1d78e02ef5c284d68c77df8dea8c657638a3 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 25 Mar 2026 22:12:16 +0100 Subject: [PATCH 34/87] polish direction gizmo - slightly change appearance to be more functional and more pleasant on the eyes - add options to customize appearance in Viewport editor options --- Source/Editor/Gizmo/DirectionGizmo.cs | 52 +++++++++++++------ Source/Editor/Options/ViewportOptions.cs | 28 ++++++++++ Source/Editor/Viewport/EditorViewport.cs | 1 - .../Viewport/MainEditorGizmoViewport.cs | 41 ++++++++++----- 4 files changed, 93 insertions(+), 29 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index babebbd5b..3620f452f 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -1,6 +1,8 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using System.Collections.Generic; +using FlaxEditor.Options; using FlaxEditor.Viewport; using FlaxEngine; using FlaxEngine.GUI; @@ -10,13 +12,19 @@ namespace FlaxEditor.Gizmo; [HideInEditor] internal class DirectionGizmo : ContainerControl { + public const float DefaultGizmoSize = 112.5f; + + private const float AxisLength = 71.25f; + private const float SpriteRadius = 10.85f; + private IGizmoOwner _owner; private ViewportProjection _viewportProjection; private EditorViewport _viewport; private Vector3 _gizmoCenter; - private float _axisLength = 75.0f; - private float _textAxisLength = 95.0f; - private float _spriteRadius = 12.0f; + private float _gizmoOpacity; + private float _backgroundOpacity; + private float _axisLength; + private float _spriteRadius; private AxisData _xAxisData; private AxisData _yAxisData; @@ -100,11 +108,26 @@ internal class DirectionGizmo : ContainerControl _axisData.EnsureCapacity(6); _spritePositions.EnsureCapacity(6); - _posHandle = Editor.Instance.Icons.VisjectBoxClosed32; - _negHandle = Editor.Instance.Icons.VisjectBoxOpen32; + var editor = Editor.Instance; + _posHandle = editor.Icons.VisjectBoxClosed32; + _negHandle = editor.Icons.VisjectBoxOpen32; _fontReference = new FontReference(Style.Current.FontSmall); - _fontReference.Size = 8; + + editor.Options.OptionsChanged += OnEditorOptionsChanged; + OnEditorOptionsChanged(editor.Options.Options); + } + + private void OnEditorOptionsChanged(EditorOptions options) + { + float gizmoScale = options.Viewport.DirectionGizmoScale; + _axisLength = AxisLength * gizmoScale; + _spriteRadius = SpriteRadius * gizmoScale; + + _gizmoOpacity = options.Viewport.DirectionGizmoOpacity; + _backgroundOpacity = options.Viewport.DirectionGizmoBackgroundOpacity; + + _fontReference.Size = 8.25f * gizmoScale; } private bool IsPointInSprite(Float2 point, Float2 spriteCenter) @@ -232,33 +255,32 @@ internal class DirectionGizmo : ContainerControl // Rebuild sprite positions list for hover detection _spritePositions.Clear(); - Render2D.DrawSprite(_posHandle, new Rectangle(0, 0, Size), Color.Black.AlphaMultiplied(0.1f)); + Render2D.DrawSprite(_posHandle, new Rectangle(0, 0, Size), Color.Black.AlphaMultiplied(_backgroundOpacity)); // Draw in order from farthest to closest for (int i = 0; i < _axisData.Count; i++) { var axis = _axisData[i]; Float2 tipScreen = relativeCenter + axis.Delta * _axisLength; - Float2 tipTextScreen = relativeCenter + axis.Delta * _textAxisLength; bool isHovered = _hoveredAxisIndex == i; // Store sprite position for hover detection - _spritePositions.Add((tipTextScreen, axis.Direction)); + _spritePositions.Add((tipScreen, axis.Direction)); var axisColor = isHovered ? new Color(1.0f, 0.8980392f, 0.039215688f) : axis.AxisColor; + axisColor.A *= _gizmoOpacity; var font = _fontReference.GetFont(); if (!axis.Negative) { - Render2D.DrawLine(relativeCenter, tipScreen, axisColor, 2.0f); - Render2D.DrawSprite(_posHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); - Render2D.DrawText(font, axis.Label, isHovered ? Color.Gray : Color.Black, tipTextScreen - font.MeasureText(axis.Label) * 0.5f); + Render2D.DrawLine(relativeCenter, tipScreen, axisColor, 1.5f); + Render2D.DrawSprite(_posHandle, new Rectangle(tipScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); + Render2D.DrawText(font, axis.Label, Color.Black, tipScreen - font.MeasureText(axis.Label) * 0.5f); } else { - Render2D.DrawSprite(_posHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor.RGBMultiplied(0.65f)); - Render2D.DrawSprite(_negHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); + Render2D.DrawSprite(_posHandle, new Rectangle(tipScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor.RGBMultiplied(0.85f).AlphaMultiplied(0.8f)); if (isHovered) - Render2D.DrawText(font, axis.Label, Color.Black, tipTextScreen - font.MeasureText(axis.Label) * 0.5f); + Render2D.DrawText(font, axis.Label, Color.Black, tipScreen - font.MeasureText(axis.Label) * 0.5f); } } diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index aed633672..6e8903bab 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -171,5 +171,33 @@ namespace FlaxEditor.Options [DefaultValue(1000.0f), Limit(0.0f, 20000.0f, 5.0f)] [EditorDisplay("Viewport Icons"), EditorOrder(410)] public float MaxSizeDistance { get; set; } = 1000.0f; + + /// + /// Gets or sets a value that indicates whether the main viewports is visible. + /// + [DefaultValue(true)] + [EditorDisplay("Direction Gizmo"), EditorOrder(500), Tooltip("Sets the visibility of the direction gizmo in the main editor viewport.")] + public bool ShowDirectionGizmo { get; set; } = true; + + /// + /// Gets or sets a value by which the main viewports size is multiplied with. + /// + [DefaultValue(1f), Limit(0.0f, 2.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(501), Tooltip("The scale of the direction gizmo in the main viewport.")] + public float DirectionGizmoScale { get; set; } = 1f; + + /// + /// Gets or sets a value for the opacity of the main viewports background. + /// + [DefaultValue(0.05f), Limit(0.0f, 1.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(502), Tooltip("The background opacity of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoBackgroundOpacity { get; set; } = 0.05f; + + /// + /// Gets or sets a value for the opacity of the main viewports . + /// + [DefaultValue(0.6f), Limit(0.0f, 1.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(503), Tooltip("The opacity of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoOpacity { get; set; } = 0.6f; } } diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 5f4c47447..ddcee7084 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -163,7 +163,6 @@ namespace FlaxEditor.Viewport private bool _useMouseAcceleration; // Input - internal bool _disableInputUpdate; private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown; private int _deltaFilteringStep; diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 6b0165190..08a4614ac 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -1,17 +1,19 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; -using System.Collections.Generic; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Options; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Modes; +using FlaxEditor.Viewport.Widgets; using FlaxEditor.Windows; using FlaxEngine; using FlaxEngine.Gizmo; using FlaxEngine.GUI; +using System; +using System.Collections.Generic; using Object = FlaxEngine.Object; namespace FlaxEditor.Viewport @@ -26,6 +28,7 @@ namespace FlaxEditor.Viewport private readonly ContextMenuButton _showGridButton; private readonly ContextMenuButton _showNavigationButton; private readonly ContextMenuButton _toggleGameViewButton; + private readonly ContextMenuButton _showDirectionGizmoButton; private SelectionOutline _customSelectionOutline; /// @@ -226,12 +229,13 @@ namespace FlaxEditor.Viewport // Add rubber band selector _rubberBandSelector = new ViewportRubberBandSelector(this); - _directionGizmo = new DirectionGizmo(this); - _directionGizmo.AnchorPreset = AnchorPresets.TopRight; - _directionGizmo.Parent = this; - _directionGizmo.LocalY += 25; - _directionGizmo.LocalX -= 150; - _directionGizmo.Size = new Float2(150, 150); + + // Add direction gizmo + _directionGizmo = new DirectionGizmo(this) + { + AnchorPreset = AnchorPresets.TopRight, + Parent = this, + }; // Add grid Grid = new GridGizmo(this); @@ -252,11 +256,10 @@ namespace FlaxEditor.Viewport _showNavigationButton.CloseMenuOnClick = false; // Show direction gizmo widget - var showDirectionGizmoButton = ViewWidgetShowMenu.AddButton("Direction Gizmo", () => _directionGizmo.Visible = !_directionGizmo.Visible); - showDirectionGizmoButton.AutoCheck = true; - showDirectionGizmoButton.CloseMenuOnClick = false; - showDirectionGizmoButton.Checked = _directionGizmo.Visible; - + _showDirectionGizmoButton = ViewWidgetShowMenu.AddButton("Direction Gizmo", () => _directionGizmo.Visible = !_directionGizmo.Visible); + _showDirectionGizmoButton.AutoCheck = true; + _showDirectionGizmoButton.CloseMenuOnClick = false; + // Game View ViewWidgetButtonMenu.AddSeparator(); _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView); @@ -290,6 +293,18 @@ namespace FlaxEditor.Viewport // Game View InputActions.Add(options => options.ToggleGameView, ToggleGameView); + + editor.Options.OptionsChanged += OnEditorOptionsChanged; + OnEditorOptionsChanged(editor.Options.Options); + } + + private void OnEditorOptionsChanged(EditorOptions options) + { + _directionGizmo.Visible = options.Viewport.ShowDirectionGizmo; + _showDirectionGizmoButton.Checked = _directionGizmo.Visible; + _directionGizmo.Size = new Float2(DirectionGizmo.DefaultGizmoSize * options.Viewport.DirectionGizmoScale); + _directionGizmo.LocalX = -_directionGizmo.Size.X * 0.5f; + _directionGizmo.LocalY = _directionGizmo.Size.Y * 0.5f + ViewportWidgetsContainer.WidgetsHeight; } /// From c9df03d29371d0a899ecc568bb7c212737f6d9cc Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 25 Mar 2026 22:15:38 +0100 Subject: [PATCH 35/87] fix viewport orientation when clicking on axis --- Source/Editor/Gizmo/DirectionGizmo.cs | 13 ++++++------- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 5 ++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index 3620f452f..282ebc974 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -1,6 +1,5 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; using System.Collections.Generic; using FlaxEditor.Options; using FlaxEditor.Viewport; @@ -179,12 +178,12 @@ internal class DirectionGizmo : ContainerControl { Quaternion orientation = direction switch { - AxisDirection.PosX => Quaternion.Euler(0, 90, 0), - AxisDirection.NegX => Quaternion.Euler(0, -90, 0), - AxisDirection.PosY => Quaternion.Euler(-90, 0, 0), - AxisDirection.NegY => Quaternion.Euler(90, 0, 0), - AxisDirection.PosZ => Quaternion.Euler(0, 0, 0), - AxisDirection.NegZ => Quaternion.Euler(0, 180, 0), + AxisDirection.PosX => Quaternion.Euler(0, -90, 0), + AxisDirection.NegX => Quaternion.Euler(0, 90, 0), + AxisDirection.PosY => Quaternion.Euler(90, 0, 0), + AxisDirection.NegY => Quaternion.Euler(-90, 0, 0), + AxisDirection.PosZ => Quaternion.Euler(0, 180, 0), + AxisDirection.NegZ => Quaternion.Euler(0, 0, 0), _ => Quaternion.Identity }; _viewport.OrientViewport(ref orientation); diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 08a4614ac..6a2a78543 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -1,5 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System.Collections.Generic; +using Object = FlaxEngine.Object; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; @@ -12,9 +14,6 @@ using FlaxEditor.Windows; using FlaxEngine; using FlaxEngine.Gizmo; using FlaxEngine.GUI; -using System; -using System.Collections.Generic; -using Object = FlaxEngine.Object; namespace FlaxEditor.Viewport { From e455c874c6a57119173ea347b47f09d1fd9e8a68 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 25 Mar 2026 22:57:17 +0100 Subject: [PATCH 36/87] tweak Z axis color and add brightness option https://discord.com/channels/437989205315158016/509056735844106251/1486480204242096369 --- Source/Editor/Gizmo/DirectionGizmo.cs | 9 +++++---- Source/Editor/Options/ViewportOptions.cs | 11 +++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index 282ebc974..b04eb8d8d 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -20,6 +20,7 @@ internal class DirectionGizmo : ContainerControl private ViewportProjection _viewportProjection; private EditorViewport _viewport; private Vector3 _gizmoCenter; + private float _gizmoBrightness; private float _gizmoOpacity; private float _backgroundOpacity; private float _axisLength; @@ -99,11 +100,11 @@ internal class DirectionGizmo : ContainerControl _xAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "X", AxisColor = new Color(1.0f, 0.0f, 0.02745f, 1.0f), Negative = false, Direction = AxisDirection.PosX }; _yAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Y", AxisColor = new Color(0.239215f, 1.0f, 0.047058f, 1.0f), Negative = false, Direction = AxisDirection.PosY }; - _zAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Z", AxisColor = new Color(0.0f, 0.0235294f, 1.0f, 1.0f), Negative = false, Direction = AxisDirection.PosZ }; + _zAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Z", AxisColor = new Color(0.0f, 0.3607f, 0.9f, 1.0f), Negative = false, Direction = AxisDirection.PosZ }; _negXAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-X", AxisColor = new Color(1.0f, 0.0f, 0.02745f, 1.0f), Negative = true, Direction = AxisDirection.NegX }; _negYAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Y", AxisColor = new Color(0.239215f, 1.0f, 0.047058f, 1.0f), Negative = true, Direction = AxisDirection.NegY }; - _negZAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Z", AxisColor = new Color(0.0f, 0.0235294f, 1.0f, 1.0f), Negative = true, Direction = AxisDirection.NegZ }; + _negZAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Z", AxisColor = new Color(0.0f, 0.3607f, 0.9f, 1.0f), Negative = true, Direction = AxisDirection.NegZ }; _axisData.EnsureCapacity(6); _spritePositions.EnsureCapacity(6); @@ -122,7 +123,7 @@ internal class DirectionGizmo : ContainerControl float gizmoScale = options.Viewport.DirectionGizmoScale; _axisLength = AxisLength * gizmoScale; _spriteRadius = SpriteRadius * gizmoScale; - + _gizmoBrightness = options.Viewport.DirectionGizmoBrightness; _gizmoOpacity = options.Viewport.DirectionGizmoOpacity; _backgroundOpacity = options.Viewport.DirectionGizmoBackgroundOpacity; @@ -267,7 +268,7 @@ internal class DirectionGizmo : ContainerControl _spritePositions.Add((tipScreen, axis.Direction)); var axisColor = isHovered ? new Color(1.0f, 0.8980392f, 0.039215688f) : axis.AxisColor; - axisColor.A *= _gizmoOpacity; + axisColor = axisColor.RGBMultiplied(_gizmoBrightness).AlphaMultiplied(_gizmoOpacity); var font = _fontReference.GetFont(); if (!axis.Negative) { diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index 6e8903bab..c7f6ba544 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -189,9 +189,9 @@ namespace FlaxEditor.Options /// /// Gets or sets a value for the opacity of the main viewports background. /// - [DefaultValue(0.05f), Limit(0.0f, 1.0f)] + [DefaultValue(0.1f), Limit(0.0f, 1.0f)] [EditorDisplay("Direction Gizmo"), EditorOrder(502), Tooltip("The background opacity of the of the direction gizmo in the main viewport.")] - public float DirectionGizmoBackgroundOpacity { get; set; } = 0.05f; + public float DirectionGizmoBackgroundOpacity { get; set; } = 0.1f; /// /// Gets or sets a value for the opacity of the main viewports . @@ -199,5 +199,12 @@ namespace FlaxEditor.Options [DefaultValue(0.6f), Limit(0.0f, 1.0f)] [EditorDisplay("Direction Gizmo"), EditorOrder(503), Tooltip("The opacity of the of the direction gizmo in the main viewport.")] public float DirectionGizmoOpacity { get; set; } = 0.6f; + + /// + /// Gets or sets a value for the opacity of the main viewports . + /// + [DefaultValue(1f), Limit(0.0f, 2.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(504), Tooltip("The brightness of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoBrightness{ get; set; } = 1f; } } From 3db3eb58a34ed8519cc1058ac8c79315af95326b Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 25 Mar 2026 23:16:27 +0100 Subject: [PATCH 37/87] add Flax 1.12 to bug report template and tweak wording for some things --- .github/ISSUE_TEMPLATE/1-bug.yaml | 8 ++++---- .github/ISSUE_TEMPLATE/2-feature-request.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1-bug.yaml b/.github/ISSUE_TEMPLATE/1-bug.yaml index a75003f63..7e6150557 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.yaml +++ b/.github/ISSUE_TEMPLATE/1-bug.yaml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! Please attach any minimal reproduction projects! + Thanks for taking the time to fill out this bug report! Please attach a minimal reproduction project if available! - type: textarea id: description-area attributes: @@ -17,19 +17,19 @@ body: id: steps-area attributes: label: Steps to reproduce - description: Please provide reproduction steps if possible. + description: Please provide reproduction steps if available. validations: required: true - type: dropdown id: version attributes: label: Version - description: What version of Flax are you running? + description: What version of Flax did you experience the bug in? options: - - '1.8' - '1.9' - '1.10' - '1.11' + - '1.12' - master branch default: 3 validations: diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yaml b/.github/ISSUE_TEMPLATE/2-feature-request.yaml index 338c9aea0..7ad43bf5b 100644 --- a/.github/ISSUE_TEMPLATE/2-feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yaml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out a feature request! + Thank you for taking the time to submit this feature request! - type: textarea id: description-area attributes: @@ -17,6 +17,6 @@ body: id: benefits-area attributes: label: Benefits - description: Please provide what benefits this feature would provide to the engine! + description: Please list what benefits this feature would provide to the engine! validations: required: true \ No newline at end of file From d0cc88f82a0fd3b1f4bc98257fde274e2e6c69b3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 14:08:03 +0100 Subject: [PATCH 38/87] Try to fix random `hdiutil` failure in Continuous Delivery on Github Action --- Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index d45d0dd4c..253d9cc14 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; +using System.Threading; using Flax.Build; using Flax.Build.Platforms; @@ -199,9 +200,14 @@ namespace Flax.Deploy var dmgPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.dmg"); Log.Info(string.Empty); Log.Info("Building disk image..."); + Thread.Sleep(100); if (File.Exists(dmgPath)) + { + Log.Verbose("Removing old image"); File.Delete(dmgPath); - Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\" -force", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + Thread.Sleep(100); + } + Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); CodeSign(dmgPath); Log.Info("Output disk image size: " + Utilities.GetFileSize(dmgPath)); From e624e1ba699e40deb05a2d63c74587d380271bd8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 14:14:15 +0100 Subject: [PATCH 39/87] Fix missing `Custom Global Code` code generation in `Defines` stage --- Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp index c473f940c..707c24416 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp @@ -456,8 +456,8 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo _writer.Write(TEXT("#define MATERIAL_REFLECTIONS_SSR_COLOR ({0})\n"), sceneColorTexture.ShaderName); } WRITE_FEATURES(Defines); - inputs[In_Defines] = _writer.ToString(); WriteCustomGlobalCode(customGlobalCodeNodes, In_Defines); + inputs[In_Defines] = _writer.ToString(); _writer.Clear(); } From ccc8d209da4ac2d2591fd9fe68ec64681671f9f1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 15:33:14 +0100 Subject: [PATCH 40/87] Fix `MeshAccessor` to validate buffer existence in C# API --- Source/Engine/Graphics/Models/MeshAccessor.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index ffe509476..1db33b5ba 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -76,6 +76,10 @@ namespace FlaxEngine /// Loaded value. public int GetInt(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return (int)_sampler.Read(data + index * _stride).X; } @@ -87,6 +91,10 @@ namespace FlaxEngine /// Loaded value. public float GetFloat(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return _sampler.Read(data + index * _stride).X; } @@ -98,6 +106,10 @@ namespace FlaxEngine /// Loaded value. public Float2 GetFloat2(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return new Float2(_sampler.Read(data + index * _stride)); } @@ -109,6 +121,10 @@ namespace FlaxEngine /// Loaded value. public Float3 GetFloat3(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return new Float3(_sampler.Read(data + index * _stride)); } @@ -120,6 +136,10 @@ namespace FlaxEngine /// Loaded value. public Float4 GetFloat4(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return _sampler.Read(data + index * _stride); } @@ -131,6 +151,10 @@ namespace FlaxEngine /// Value to assign. public void SetInt(int index, int value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value); fixed (byte* data = _data) _sampler.Write(data + index * _stride, ref v); @@ -143,6 +167,10 @@ namespace FlaxEngine /// Value to assign. public void SetFloat(int index, float value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value); fixed (byte* data = _data) _sampler.Write(data + index * _stride, ref v); @@ -155,6 +183,10 @@ namespace FlaxEngine /// Value to assign. public void SetFloat2(int index, Float2 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value, 0.0f, 0.0f); fixed (byte* data = _data) _sampler.Write(data + index * _stride, ref v); @@ -167,6 +199,10 @@ namespace FlaxEngine /// Value to assign. public void SetFloat3(int index, Float3 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value, 0.0f); fixed (byte* data = _data) _sampler.Write(data + index * _stride, ref v); @@ -179,6 +215,10 @@ namespace FlaxEngine /// Value to assign. public void SetFloat4(int index, Float4 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) _sampler.Write(data + index * _stride, ref value); } @@ -190,6 +230,10 @@ namespace FlaxEngine /// Pointer to the source data. public void SetLinear(IntPtr data) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif new Span(data.ToPointer(), _data.Length).CopyTo(_data); } @@ -199,6 +243,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -223,6 +271,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -247,6 +299,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32A32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -271,6 +327,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32_UInt)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -304,6 +364,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -325,6 +389,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -346,6 +414,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32A32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -367,6 +439,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32_UInt)) { _data.CopyTo(MemoryMarshal.Cast(dst)); From f1b6c13ba9d63e2634c5f6206cc5252b01ea70a0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 15:33:55 +0100 Subject: [PATCH 41/87] Fix PixelFormatSampler.Write` in C# to actually work --- Source/Engine/Graphics/Models/MeshAccessor.cs | 18 +++++++++--------- Source/Engine/Graphics/PixelFormatSampler.cs | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 1db33b5ba..8e9050e5a 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -157,7 +157,7 @@ namespace FlaxEngine #endif var v = new Float4(value); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -173,7 +173,7 @@ namespace FlaxEngine #endif var v = new Float4(value); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -189,7 +189,7 @@ namespace FlaxEngine #endif var v = new Float4(value, 0.0f, 0.0f); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -205,7 +205,7 @@ namespace FlaxEngine #endif var v = new Float4(value, 0.0f); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -220,7 +220,7 @@ namespace FlaxEngine throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); #endif fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref value); + _sampler.Write(data + index * _stride, &value); } /// @@ -259,7 +259,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i], 0.0f, 0.0f); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -287,7 +287,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i], 0.0f); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -315,7 +315,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = (Float4)src[i]; - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -352,7 +352,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i]); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } diff --git a/Source/Engine/Graphics/PixelFormatSampler.cs b/Source/Engine/Graphics/PixelFormatSampler.cs index 8ff5a32fb..62046b2a5 100644 --- a/Source/Engine/Graphics/PixelFormatSampler.cs +++ b/Source/Engine/Graphics/PixelFormatSampler.cs @@ -22,7 +22,7 @@ namespace FlaxEngine /// /// Write data function. /// - public delegate* unmanaged Write; + public delegate* unmanaged Write; /// /// Tries to get a sampler tool for the specified format to read pixels. @@ -38,7 +38,7 @@ namespace FlaxEngine Format = format, PixelSize = pixelSize, Read = (delegate* unmanaged)read.ToPointer(), - Write = (delegate* unmanaged)write.ToPointer(), + Write = (delegate* unmanaged)write.ToPointer(), }; return pixelSize != 0; } From b6789ee5236ad953eaf10bf8f43ff670a81bb00f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 17:13:46 +0100 Subject: [PATCH 42/87] Add various improvements to `MeshAccessor` such as `ComputeNormals` and `ComputeTangents` https://forum.flaxengine.com/t/how-to-create-procedural-generated-geometry-using-meshaccessor/2420 --- .../Engine/Graphics/GPUBufferDescription.cs | 14 +++ Source/Engine/Graphics/Models/MeshAccessor.cs | 92 ++++++++++++++++++- Source/Engine/Graphics/Models/MeshAccessor.h | 5 + Source/Engine/Graphics/RenderTools.cpp | 7 ++ Source/Engine/Graphics/RenderTools.h | 12 +-- 5 files changed, 119 insertions(+), 11 deletions(-) diff --git a/Source/Engine/Graphics/GPUBufferDescription.cs b/Source/Engine/Graphics/GPUBufferDescription.cs index 107d17b3e..43fcf94bf 100644 --- a/Source/Engine/Graphics/GPUBufferDescription.cs +++ b/Source/Engine/Graphics/GPUBufferDescription.cs @@ -428,6 +428,20 @@ namespace FlaxEngine partial struct VertexElement : IEquatable { + /// + /// Creates the vertex element description. + /// + /// Element type. + /// Data format. + public VertexElement(Types type, PixelFormat format) + { + Type = type; + Slot = 0; + Offset = 0; + PerInstance = 0; + Format = format; + } + /// /// Creates the vertex element description. /// diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 8e9050e5a..b1a7eebd4 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace FlaxEngine @@ -466,6 +467,17 @@ namespace FlaxEngine } } } + + /// + /// Checks if stream is valid. + /// + /// The stream to check. + /// True if stream is valid, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(Stream stream) + { + return stream.IsValid; + } } private byte[][] _data = new byte[(int)MeshBufferType.MAX][]; @@ -586,19 +598,19 @@ namespace FlaxEngine bool use16BitIndexBuffer = false; IntPtr[] vbData = new IntPtr[3]; GPUVertexLayout[] vbLayout = new GPUVertexLayout[3]; - if (_data[VB0] != null) + if (_data[VB0] != null && _data[VB0].Length != 0) { vbData[0] = dataPtr[VB0]; vbLayout[0] = _layouts[VB0]; vertices = (uint)_data[VB0].Length / _layouts[VB0].Stride; } - if (_data[VB1] != null) + if (_data[VB1] != null && _data[VB1].Length != 0) { vbData[1] = dataPtr[VB1]; vbLayout[1] = _layouts[VB1]; vertices = (uint)_data[VB1].Length / _layouts[VB1].Stride; } - if (_data[VB2] != null) + if (_data[VB2] != null && _data[VB2].Length != 0) { vbData[2] = dataPtr[VB2]; vbLayout[2] = _layouts[VB2]; @@ -798,6 +810,16 @@ namespace FlaxEngine set => SetStreamFloat3(VertexElement.Types.Normal, value, PackNormal); } + /// + /// Gets or sets the vertex tangent vectors (unpacked, normalized). Null if does not exist in vertex buffers of the mesh. + /// + /// Uses stream to read or write data to the vertex buffer. + public Float3[] Tangents + { + get => GetStreamFloat3(VertexElement.Types.Tangent, UnpackNormal); + set => SetStreamFloat3(VertexElement.Types.Tangent, value, PackNormal); + } + /// /// Gets or sets the vertex UVs (texcoord channel 0). Null if does not exist in vertex buffers of the mesh. /// @@ -808,6 +830,70 @@ namespace FlaxEngine set => SetStreamFloat2(VertexElement.Types.TexCoord, value); } + /// + /// Recalculates normal vectors for all vertices. + /// + public void ComputeNormals() + { + var positions = Position(); + var indices = Index(); + if (!positions) + throw new Exception("Cannot compute tangents without positions."); + if (!indices) + throw new Exception("Cannot compute tangents without indices."); + if (!Normal()) + throw new Exception("Cannot compute tangents without Normal vertex element."); + var vertexCount = positions.Count; + if (vertexCount == 0) + return; + var indexCount = indices.Count; + + // Compute per-face normals but store them per-vertex + var normals = new Float3[vertexCount]; + for (int i = 0; i < indexCount; i += 3) + { + var i1 = indices.GetInt(i + 0); + var i2 = indices.GetInt(i + 1); + var i3 = indices.GetInt(i + 2); + Float3 v1 = positions.GetFloat3(i1); + Float3 v2 = positions.GetFloat3(i2); + Float3 v3 = positions.GetFloat3(i3); + Float3 n = Float3.Cross((v2 - v1), (v3 - v1)).Normalized; + + normals[i1] += n; + normals[i2] += n; + normals[i3] += n; + } + + // Average normals + for (int i = 0; i < normals.Length; i++) + normals[i] = normals[i].Normalized; + + // Write back to the buffer + Normals = normals; + } + + /// + /// Recalculates tangent vectors for all vertices based on normals. + /// + public void ComputeTangents() + { + var normals = Normal(); + var tangents = Tangent(); + if (!normals) + throw new Exception("Cannot compute tangents without normals."); + if (!tangents) + throw new Exception("Cannot compute tangents without Tangent vertex element."); + var count = normals.Count; + for (int i = 0; i < count; i++) + { + Float3 normal = normals.GetFloat3(i); + UnpackNormal(ref normal); + RenderTools.CalculateTangentFrame(out var n, out var t, ref normal); + tangents.SetFloat4(i, t); + } + } + private uint[] GetStreamUInt(Stream stream) { uint[] result = null; diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h index 595f76c6b..8bedfdc7f 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.h +++ b/Source/Engine/Graphics/Models/MeshAccessor.h @@ -38,6 +38,11 @@ public: bool IsValid() const; bool IsLinear(PixelFormat expectedFormat) const; + FORCE_INLINE operator bool() const + { + return IsValid(); + } + FORCE_INLINE int32 GetInt(int32 index) const { ASSERT_LOW_LAYER(index * _stride < _data.Length()); diff --git a/Source/Engine/Graphics/RenderTools.cpp b/Source/Engine/Graphics/RenderTools.cpp index effbe6e1b..2cf1c9707 100644 --- a/Source/Engine/Graphics/RenderTools.cpp +++ b/Source/Engine/Graphics/RenderTools.cpp @@ -548,6 +548,13 @@ void RenderTools::ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascad } } +bool RenderTools::ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame, bool updateForce) +{ + int32 updateFrequency, updatePhrase; + ComputeCascadeUpdateFrequency(cascadeIndex, cascadeCount, updateFrequency, updatePhrase, updateMaxCountPerFrame); + return (frameIndex % updateFrequency == updatePhrase) || updateForce; +} + float RenderTools::ComputeTemporalTime() { const float time = Time::Draw.UnscaledTime.GetTotalSeconds(); diff --git a/Source/Engine/Graphics/RenderTools.h b/Source/Engine/Graphics/RenderTools.h index 5f0dc23dc..0608c4338 100644 --- a/Source/Engine/Graphics/RenderTools.h +++ b/Source/Engine/Graphics/RenderTools.h @@ -119,12 +119,7 @@ public: static void ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascadeCount, int32& updateFrequency, int32& updatePhrase, int32 updateMaxCountPerFrame = 1); // Checks if cached data should be updated during the given frame. - FORCE_INLINE static bool ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame = 1, bool updateForce = false) - { - int32 updateFrequency, updatePhrase; - ComputeCascadeUpdateFrequency(cascadeIndex, cascadeCount, updateFrequency, updatePhrase, updateMaxCountPerFrame); - return (frameIndex % updateFrequency == updatePhrase) || updateForce; - } + static bool ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame = 1, bool updateForce = false); // Calculates temporal offset in the dithering factor that gets cleaned out by TAA. // Returns 0-1 value based on unscaled draw time for temporal effects to reduce artifacts from screen-space dithering when using Temporal Anti-Aliasing. @@ -136,8 +131,9 @@ public: DEPRECATED("Use CalculateTangentFrame with unpacked Float3/Float4.") static void CalculateTangentFrame(FloatR10G10B10A2& resultNormal, FloatR10G10B10A2& resultTangent, const Float3& normal, const Float3& tangent); // Result normal/tangent are already packed into [0;1] range. - static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal); - static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal, const Float3& tangent); + API_FUNCTION() static void CalculateTangentFrame(API_PARAM(Out) Float3& resultNormal, API_PARAM(Out) Float4& resultTangent, API_PARAM(Ref) const Float3& normal); + // Result normal/tangent are already packed into [0;1] range. + API_FUNCTION() static void CalculateTangentFrame(API_PARAM(Out) Float3& resultNormal, API_PARAM(Out) Float4& resultTangent, API_PARAM(Ref) const Float3& normal, API_PARAM(Ref) const Float3& tangent); static void ComputeSphereModelDrawMatrix(const RenderView& view, const Float3& position, float radius, Matrix& resultWorld, bool& resultIsViewInside); From 09a05739321a5793ce6eb1b3e56e202059e92603 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 17:14:14 +0100 Subject: [PATCH 43/87] Fix `MeshAccessor.Stream` count for items not aligned to buffer start --- Source/Engine/Graphics/Models/MeshAccessor.cs | 17 ++++++++++------- Source/Engine/Graphics/Models/MeshAccessor.h | 4 ++-- Source/Engine/Graphics/Models/MeshBase.cpp | 15 +++++++++------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index b1a7eebd4..cd216cdf5 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -18,13 +18,14 @@ namespace FlaxEngine { private Span _data; private PixelFormat _format; - private int _stride; + private int _stride, _count; private readonly PixelFormatSampler _sampler; - internal Stream(Span data, PixelFormat format, int stride) + internal Stream(Span data, PixelFormat format, int stride, int count) { _data = data; _stride = stride; + _count = count; if (PixelFormatSampler.Get(format, out _sampler)) { _format = format; @@ -53,7 +54,7 @@ namespace FlaxEngine /// /// Gets the count of the items in the stride. /// - public int Count => _data.Length / _stride; + public int Count => _count; /// /// Returns true if stream is valid. @@ -664,15 +665,16 @@ namespace FlaxEngine { Span data = new Span(); PixelFormat format = PixelFormat.Unknown; - int stride = 0; + int stride = 0, count = 0; var ib = _data[(int)MeshBufferType.Index]; if (ib != null) { data = ib; format = _formats[(int)MeshBufferType.Index]; stride = PixelFormatExtensions.SizeInBytes(format); + count = data.Length / stride; } - return new Stream(data, format, stride); + return new Stream(data, format, stride, count); } /// @@ -684,7 +686,7 @@ namespace FlaxEngine { Span data = new Span(); PixelFormat format = PixelFormat.Unknown; - int stride = 0; + int stride = 0, count = 0; for (int vbIndex = 0; vbIndex < 3 && format == PixelFormat.Unknown; vbIndex++) { int idx = vbIndex + 1; @@ -699,11 +701,12 @@ namespace FlaxEngine data = new Span(vb).Slice(e.Offset); format = e.Format; stride = (int)layout.Stride; + count = vb.Length / stride; break; } } } - return new Stream(data, format, stride); + return new Stream(data, format, stride, count); } /// diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h index 8bedfdc7f..f0b40b623 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.h +++ b/Source/Engine/Graphics/Models/MeshAccessor.h @@ -25,10 +25,10 @@ public: private: Span _data; PixelFormat _format; - int32 _stride; + int32 _stride, _count; PixelFormatSampler _sampler; - Stream(Span data, PixelFormat format, int32 stride); + Stream(Span data, PixelFormat format, int32 stride, int32 count); public: Span GetData() const; diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index 81fffb3e7..435f6d0c8 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -38,10 +38,11 @@ namespace #endif } -MeshAccessor::Stream::Stream(Span data, PixelFormat format, int32 stride) +MeshAccessor::Stream::Stream(Span data, PixelFormat format, int32 stride, int32 count) : _data(data) , _format(PixelFormat::Unknown) , _stride(stride) + , _count(count) { auto sampler = PixelFormatSampler::Get(format); if (sampler) @@ -72,7 +73,7 @@ int32 MeshAccessor::Stream::GetStride() const int32 MeshAccessor::Stream::GetCount() const { - return _data.Length() / _stride; + return _count; } bool MeshAccessor::Stream::IsValid() const @@ -368,22 +369,23 @@ MeshAccessor::Stream MeshAccessor::Index() { Span data; PixelFormat format = PixelFormat::Unknown; - int32 stride = 0; + int32 stride = 0, count = 0; auto& ib = _data[(int32)MeshBufferType::Index]; if (ib.IsValid()) { data = ib; format = _formats[(int32)MeshBufferType::Index]; stride = PixelFormatExtensions::SizeInBytes(format); + count = data.Length() / stride; } - return Stream(data, format, stride); + return Stream(data, format, stride, count); } MeshAccessor::Stream MeshAccessor::Attribute(VertexElement::Types attribute) { Span data; PixelFormat format = PixelFormat::Unknown; - int32 stride = 0; + int32 stride = 0, count = 0; for (int32 vbIndex = 0; vbIndex < 3 && format == PixelFormat::Unknown; vbIndex++) { static_assert((int32)MeshBufferType::Vertex0 == 1, "Update code."); @@ -399,11 +401,12 @@ MeshAccessor::Stream MeshAccessor::Attribute(VertexElement::Types attribute) data = vb.Slice(e.Offset); format = e.Format; stride = layout->GetStride(); + count = vb.Length() / stride; break; } } } - return Stream(data, format, stride); + return Stream(data, format, stride, count); } MeshBase::~MeshBase() From 58586ca4f0ebf6bfd4cef8937fefea1b28a8229a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Mar 2026 17:17:13 +0100 Subject: [PATCH 44/87] Add small improvement to terrain normals --- Source/Engine/Terrain/TerrainPatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index cadc9821c..e579c05f7 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -439,7 +439,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh // Calculate normals for quad two vertices Float3 n0 = Float3::Normalize((v00 - v01) ^ (v01 - v10)); Float3 n1 = Float3::Normalize((v11 - v10) ^ (v10 - v01)); - Float3 n2 = n0 + n1; + Float3 n2 = Float3::Normalize(n0 + n1); // Apply normal to each vertex using it normalsPerVertex[i00] += n1; From f5651de725d320b8a1dec22a03b463e1672d30ea Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Mar 2026 10:16:32 +0100 Subject: [PATCH 45/87] Add NDK 27 as minimum for Android to fix 16kb page alignment issue on `libc++_shared.so` --- .../Tools/Flax.Build/Platforms/Android/AndroidNdk.cs | 11 ++++++++++- .../Flax.Build/Platforms/Android/AndroidToolchain.cs | 7 +++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs index a1018368b..00838a73d 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs @@ -67,7 +67,8 @@ namespace Flax.Build.Platforms if (subDirs.Length != 0) { Utilities.SortVersionDirectories(subDirs); - FindNDK(subDirs.Last()); + for (int i = subDirs.Length - 1; i >= 0 && !IsValid; i--) + FindNDK(subDirs[i]); } if (!IsValid) @@ -109,6 +110,14 @@ namespace Flax.Build.Platforms } if (IsValid) { + var minVersion = new Version(27, 0); // NDK 27 (and newer) contains the libc++_shared.so with 16kb alignment + if (Version < minVersion) + { + IsValid = false; + Log.Verbose(RootPath); + Log.Error(string.Format("Unsupported Android NDK version {0}. Minimum supported is {1}.", Version, minVersion)); + return; + } RootPath = sdkPath; Log.Info(string.Format("Found Android NDK {1} at {0}", RootPath, Version)); } diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs index d88cdbd51..9448a7c4d 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs @@ -104,6 +104,9 @@ namespace Flax.Build.Platforms args.Add("-fno-function-sections"); } + // Support 16kb pages + args.Add("-D__BIONIC_NO_PAGE_SIZE_MACRO"); + switch (Architecture) { case TargetArchitecture.x86: @@ -195,10 +198,10 @@ namespace Flax.Build.Platforms { // https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#libc var ndkPath = AndroidNdk.Instance.RootPath; - var libCppSharedPath = Path.Combine(ndkPath, "sources/cxx-stl/llvm-libc++/libs/", GetAbiName(Architecture), "libc++_shared.so"); // NDK24 (and older) location + var libCppSharedPath = Path.Combine(ndkPath, "toolchains/llvm/prebuilt", AndroidSdk.GetHostName(), "sysroot/usr/lib/", GetToolchainName(TargetPlatform.Android, Architecture), "libc++_shared.so"); // NDK25+ location if (!File.Exists(libCppSharedPath)) { - libCppSharedPath = Path.Combine(ndkPath, "toolchains/llvm/prebuilt", AndroidSdk.GetHostName(), "sysroot/usr/lib/", GetToolchainName(TargetPlatform.Android, Architecture), "libc++_shared.so"); // NDK25+ location + libCppSharedPath = Path.Combine(ndkPath, "sources/cxx-stl/llvm-libc++/libs/", GetAbiName(Architecture), "libc++_shared.so"); // NDK24 (and older) location if (!File.Exists(libCppSharedPath)) throw new Exception($"Missing Android NDK `libc++_shared.so` for architecture {Architecture}."); } From 8f50d9faec925df6e3c878b2e603550da6165d4b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Mar 2026 11:06:11 +0100 Subject: [PATCH 46/87] Fix terrain collision geometry order to match heights buffer #3844 --- Source/Engine/Terrain/TerrainPatch.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index e579c05f7..164fce747 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -2572,9 +2572,9 @@ void TerrainPatch::ExtractCollisionGeometry(Array& vertexBuffer, Array& vertexBuffer, Array Date: Fri, 27 Mar 2026 11:16:25 +0100 Subject: [PATCH 47/87] Bump up build number --- Flax.flaxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index 78c03fce6..dc126772a 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 11, "Revision": 0, - "Build": 6808 + "Build": 6809 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", From 6c4fc44163c494afaf726731fa79dcc9d15ac50c Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 27 Mar 2026 10:59:06 -0500 Subject: [PATCH 48/87] Fix arrow rect issue. --- .../Content/Tree/ContentFolderTreeNode.cs | 27 +++++++++++++++++++ Source/Editor/Windows/ContentWindow.cs | 1 + 2 files changed, 28 insertions(+) diff --git a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs index 129cc1473..6741910cf 100644 --- a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs @@ -100,8 +100,35 @@ public class ContentFolderTreeNode : TreeNode Parent = parent; } IconColor = Color.Transparent; // Hide default icon, we draw scaled icon manually + UpdateCustomArrowRect(); Editor.Instance?.Windows?.ContentWin?.TryAutoExpandContentNode(this); } + + /// + /// Updates the custom arrow rectangle so it stays aligned with the current layout. + /// + private void UpdateCustomArrowRect() + { + var contentWindow = Editor.Instance?.Windows?.ContentWin; + var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f; + var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f); + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + + // Use the current text layout, not just cached values. + var textRect = TextRect; + var iconLeft = textRect.Left - iconSize - 2.0f; + var x = Mathf.Max(iconLeft - arrowSize - 2.0f, 0.0f); + var y = Mathf.Max((HeaderHeight - arrowSize) * 0.5f, 0.0f); + + CustomArrowRect = new Rectangle(x, y, arrowSize, arrowSize); + } + + /// + public override void PerformLayout(bool force = false) + { + base.PerformLayout(force); + UpdateCustomArrowRect(); + } /// /// Shows the rename popup for the item. diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 1a4b3667d..f61dfa903 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -1473,6 +1473,7 @@ namespace FlaxEditor.Windows var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); var textMarginLeft = 2.0f + Mathf.Max(0.0f, iconSize - 16.0f); ApplyTreeNodeScale(_root, headerHeight, fontRef, textMarginLeft); + _root?.PerformLayout(true); _tree.PerformLayout(); } From 2b538e4864290335da922a385e8c09b51766f013 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Mar 2026 23:19:27 +0100 Subject: [PATCH 49/87] Fix direction gizmo size on different FOV and fix event #4025 --- Source/Editor/Gizmo/DirectionGizmo.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index b04eb8d8d..b76cd0607 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -157,9 +157,9 @@ internal class DirectionGizmo : ContainerControl } /// - public override bool OnMouseDown(Float2 location, MouseButton button) + public override bool OnMouseUp(Float2 location, MouseButton button) { - if (base.OnMouseDown(location, button)) + if (base.OnMouseUp(location, button)) return true; // Check which axis is being clicked - check from closest to farthest for proper layering @@ -215,8 +215,19 @@ internal class DirectionGizmo : ContainerControl // Normalize by viewport height to keep size independent of FOV and viewport dimensions float heightNormalization = _viewport.Height / 720.0f; // 720 = reference height + + // Fix in axes distance no matter FOV/OrthoScale to keep consistent size regardless of zoom level if (_owner.Viewport.UseOrthographicProjection) - heightNormalization /= _owner.Viewport.OrthographicScale * 0.5f; // Fix in ortho view to keep consistent size regardless of zoom level + heightNormalization /= _owner.Viewport.OrthographicScale * 0.5f; + else + { + // This could be some actual math expression, not that hack + var fov = _owner.Viewport.FieldOfView / 60.0f; + float scaleAt30 = 0.1f, scaleAt60 = 1.0f, scaleAt120 = 1.5f, scaleAt180 = 3.0f; + heightNormalization /= Mathf.Lerp(scaleAt30, scaleAt60, fov); + heightNormalization /= Mathf.Lerp(scaleAt60, scaleAt120, Mathf.Saturate(fov - 1)); + heightNormalization /= Mathf.Lerp(scaleAt60, scaleAt180, Mathf.Saturate(fov - 2)); + } Float2 xDelta = (xProjected - gizmoCenterScreen) / heightNormalization; Float2 yDelta = (yProjected - gizmoCenterScreen) / heightNormalization; From 258ba233d54677090943bcdca639c1b64be99ced Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Mar 2026 23:52:11 +0100 Subject: [PATCH 50/87] Attempt to fix time precision issue in long game run #3865 --- Source/Engine/Core/Types/TimeSpan.h | 2 +- Source/Engine/Graphics/Materials/MaterialShader.cpp | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Core/Types/TimeSpan.h b/Source/Engine/Core/Types/TimeSpan.h index 7704007a5..7455b7582 100644 --- a/Source/Engine/Core/Types/TimeSpan.h +++ b/Source/Engine/Core/Types/TimeSpan.h @@ -253,7 +253,7 @@ public: /// FORCE_INLINE float GetTotalSeconds() const { - return static_cast(Ticks) / TicksPerSecond; + return (float)((double)Ticks / TicksPerSecond); } public: diff --git a/Source/Engine/Graphics/Materials/MaterialShader.cpp b/Source/Engine/Graphics/Materials/MaterialShader.cpp index 5d929320d..3e26d954f 100644 --- a/Source/Engine/Graphics/Materials/MaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/MaterialShader.cpp @@ -50,13 +50,10 @@ IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderC } IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderContext& renderContext, const ::DrawCall& drawCall, bool instanced) - : GPUContext(context) - , RenderContext(renderContext) - , DrawCall(&drawCall) - , Time(Time::Draw.UnscaledTime.GetTotalSeconds()) - , ScaledTime(Time::Draw.Time.GetTotalSeconds()) - , Instanced(instanced) + : BindParameters(context, renderContext) { + DrawCall = &drawCall; + Instanced = instanced; } GPUConstantBuffer* IMaterial::BindParameters::PerViewConstants = nullptr; From e99dc8dce4da1de14410ea4eae58361058e04ce2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Mar 2026 23:54:43 +0100 Subject: [PATCH 51/87] Fix missing doc warning --- Source/Editor/Content/Tree/ContentItemTreeNode.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Editor/Content/Tree/ContentItemTreeNode.cs b/Source/Editor/Content/Tree/ContentItemTreeNode.cs index 43dbe278a..09d1ec01b 100644 --- a/Source/Editor/Content/Tree/ContentItemTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentItemTreeNode.cs @@ -202,6 +202,9 @@ public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner base.OnDestroy(); } + /// + /// Updates the text of the node. + /// public void UpdateDisplayedName() { var contentWindow = Editor.Instance?.Windows?.ContentWin; From c12948c6cc84ae6590e4184622ac899716792161 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Mar 2026 00:04:03 +0100 Subject: [PATCH 52/87] Fix crash when applying prefab changes using default instance (invalid) #3754 --- Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 378b706ed..5136a6cce 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -650,6 +650,11 @@ bool Prefab::ApplyAll(Actor* targetActor) LOG(Warning, "Failed to create default prefab instance for the prefab asset."); return true; } + if (targetActor == _defaultInstance || targetActor->HasActorInHierarchy(_defaultInstance) || _defaultInstance->HasActorInHierarchy(targetActor)) + { + LOG(Error, "Cannot apply changes to the prefab using default instance. Use manually spawned prefab instance instead."); + return true; + } if (targetActor->GetPrefabObjectID() != GetRootObjectId()) { LOG(Warning, "Applying prefab changes with modified root object. Root object id: {0}, new root: {1} (prefab object id: {2})", GetRootObjectId().ToString(), targetActor->ToString(), targetActor->GetPrefabObjectID()); From 76378156f7b03b528a097b1dd10dd68d147ccec7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Mar 2026 00:31:46 +0100 Subject: [PATCH 53/87] Fix crash when applying prefab changes #3755 --- Source/Editor/CustomEditors/Dedicated/ActorEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index d312f64b2..31e1490d6 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -203,7 +203,7 @@ namespace FlaxEditor.CustomEditors.Dedicated //Presenter.BuildLayoutOnUpdate(); // Better way is to just update the reference value using the new default instance of the prefab, created after changes apply - if (Values != null && prefab && !prefab.WaitForLoaded()) + if (Values != null && (Actor)Values[0] && prefab && !prefab.WaitForLoaded()) { var actor = (Actor)Values[0]; var prefabObjectId = actor.PrefabObjectID; From f26dbf530c60e2a245ea15c692cb9032aae9689f Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 28 Mar 2026 13:46:38 +0100 Subject: [PATCH 54/87] fix skeleton blend mask node to resize when asset reference is (dis-) connected --- Source/Editor/Surface/Archetypes/Animation.cs | 3 +++ Source/Editor/Surface/SurfaceNode.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index a3fc66b82..c8d45cc5f 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -70,6 +70,9 @@ namespace FlaxEditor.Surface.Archetypes if (box.ID != _assetBox.ID) return; _assetSelect.Visible = !box.HasAnyConnection; + + if (!Archetype.UseFixedSize) + ResizeAuto(); } } diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index b37d85d3c..2d440160f 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -1058,7 +1058,7 @@ namespace FlaxEditor.Surface } /// - /// Sets teh node values from the given pasted source. Can be overriden to perform validation or custom values processing. + /// Sets teh node values from the given pasted source. Can be overridden to perform validation or custom values processing. /// /// The input values array. public virtual void SetValuesPaste(object[] values) From 095f7277e244266d064b41d4e5020cb002f5bb43 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Mar 2026 15:13:52 +0100 Subject: [PATCH 55/87] Minor fixes --- .../CustomEditors/Dedicated/ActorEditor.cs | 5 ++--- .../Windows/ContentWindow.Navigation.cs | 20 ++++++++++++------- Source/Editor/Windows/ContentWindow.cs | 2 -- Source/Engine/Level/Prefabs/Prefab.h | 2 ++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index 31e1490d6..def57b332 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -55,9 +55,8 @@ namespace FlaxEditor.CustomEditors.Dedicated { // TODO: consider editing more than one instance of the same prefab asset at once - var prefab = FlaxEngine.Content.LoadAsync(actor.PrefabID); - // TODO: don't stall here? - if (prefab && !prefab.WaitForLoaded()) + var prefab = FlaxEngine.Content.Load(actor.PrefabID); + if (prefab) { var prefabObjectId = actor.PrefabObjectID; var prefabInstance = prefab.GetDefaultInstance(ref prefabObjectId); diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs index cc9429d47..4c7373aa6 100644 --- a/Source/Editor/Windows/ContentWindow.Navigation.cs +++ b/Source/Editor/Windows/ContentWindow.Navigation.cs @@ -14,6 +14,7 @@ namespace FlaxEditor.Windows private void OnTreeSelectionChanged(List from, List to) { + bool setLastViewFolder = !IsLayoutLocked; if (!_showAllContentInTree && to.Count > 1) { _tree.Select(to[^1]); @@ -21,11 +22,14 @@ namespace FlaxEditor.Windows } if (_showAllContentInTree && to.Count > 1) { - var activeNode = GetActiveTreeSelection(to); - if (activeNode is ContentItemTreeNode itemNode) - SaveLastViewedFolder(itemNode.Item?.ParentFolder?.Node); - else - SaveLastViewedFolder(activeNode as ContentFolderTreeNode); + if (setLastViewFolder) + { + var activeNode = GetActiveTreeSelection(to); + if (activeNode is ContentItemTreeNode itemNode) + SaveLastViewedFolder(itemNode.Item?.ParentFolder?.Node); + else + SaveLastViewedFolder(activeNode as ContentFolderTreeNode); + } UpdateUI(); return; } @@ -35,7 +39,8 @@ namespace FlaxEditor.Windows var targetNode = GetActiveTreeSelection(to); if (targetNode is ContentItemTreeNode itemNode2) { - SaveLastViewedFolder(itemNode2.Item?.ParentFolder?.Node); + if (setLastViewFolder) + SaveLastViewedFolder(itemNode2.Item?.ParentFolder?.Node); UpdateUI(); itemNode2.Focus(); return; @@ -44,7 +49,8 @@ namespace FlaxEditor.Windows var target = targetNode as ContentFolderTreeNode; Navigate(source, target); - SaveLastViewedFolder(target); + if (setLastViewFolder) + SaveLastViewedFolder(target); target?.Focus(); } diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index f61dfa903..2141b283e 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -1227,8 +1227,6 @@ namespace FlaxEditor.Windows UpdateItemsSearch(); else RefreshView(SelectedNode); - - return; } /// diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h index 9e6023a82..cde99a2cb 100644 --- a/Source/Engine/Level/Prefabs/Prefab.h +++ b/Source/Engine/Level/Prefabs/Prefab.h @@ -60,12 +60,14 @@ public: /// /// Requests the default prefab object instance. Deserializes the prefab objects from the asset. Skips if already done. /// + /// Default instances of the prefab are read-only and are used internally for objects serialization (prefab diff). /// The root of the prefab object loaded from the prefab. Contains the default values. It's not added to gameplay but deserialized with postLoad and init event fired. API_FUNCTION() Actor* GetDefaultInstance(); /// /// Requests the default prefab object instance. Deserializes the prefab objects from the asset. Skips if already done. /// + /// Default instances of the prefab are read-only and are used internally for objects serialization (prefab diff). /// The ID of the object to get from prefab default object. It can be one of the child-actors or any script that exists in the prefab. Methods returns root if id is empty. /// The object of the prefab loaded from the prefab. Contains the default values. It's not added to gameplay but deserialized with postLoad and init event fired. API_FUNCTION() SceneObject* GetDefaultInstance(API_PARAM(Ref) const Guid& objectId); From 9fa9e75e0801fa9a3242d96fa1021c8cce27ad1b Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 28 Mar 2026 21:28:09 +0100 Subject: [PATCH 56/87] fix decorator size when surface is loaded --- Source/Editor/Surface/Archetypes/BehaviorTree.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index c112bcf27..d99fedaee 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -829,6 +829,11 @@ namespace FlaxEditor.Surface.Archetypes node.ResizeAuto(); } } + else + { + // Correctly size decorators when surface is loaded + Node.ResizeAuto(); + } } /// From b78ff57f6ab4ac10382adf6f35c78c0a14d650b2 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 28 Mar 2026 22:04:41 +0100 Subject: [PATCH 57/87] fix custom code nodes --- Source/Editor/Surface/Archetypes/Material.cs | 17 +++++++++-------- Source/Editor/Surface/Archetypes/Tools.cs | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index a5c7b1da7..719553651 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -312,16 +312,16 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch) { _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array - Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 25f, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; + Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; if (nodeArch.TypeID == 8) { - pos += new Float2(60, 0); - size = new Float2(125, 200); + pos += new Float2(65, 0); + size = new Float2(160, 185); } else { - pos += new Float2(0, 40); - size = new Float2(300, 200); + pos += new Float2(0, 40 + FlaxEditor.Utilities.Constants.UIMargin * 2f); + size = new Float2(300, 180); } _textBox = new CustomCodeTextBox { @@ -506,8 +506,8 @@ namespace FlaxEditor.Surface.Archetypes Size = new Float2(300, 200), DefaultValues = new object[] { - "// Here you can add HLSL code\nOutput0 = Input0;", - new Float2(300, 200), + "// You can add HLSL code here\nOutput0 = Input0;", + new Float2(350, 200), }, Elements = new[] { @@ -969,10 +969,11 @@ namespace FlaxEditor.Surface.Archetypes Title = "Custom Global Code", Description = "Custom global HLSL shader code expression (placed before material shader code). Can contain includes to shader utilities or declare functions to reuse later.", Flags = NodeFlags.MaterialGraph, + UseFixedSize = true, Size = new Float2(300, 240), DefaultValues = new object[] { - "// Here you can add HLSL code\nfloat4 GetCustomColor()\n{\n\treturn float4(1, 0, 0, 1);\n}", + "// You can add HLSL code here\nfloat4 GetCustomColor()\n{\n\treturn float4(1, 0, 0, 1);\n}", true, (int)MaterialTemplateInputsMapping.Utilities, new Float2(300, 240), diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index bd5818693..757c9f5c9 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -486,7 +486,7 @@ namespace FlaxEditor.Surface.Archetypes zero, // Tangent In zero, // Tangent Out - // Empty keys zero-6 + // Empty keys 0-6 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, From ec756fe6266b805aa9dcbfd984de5f8a55f3f7a8 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 28 Mar 2026 22:16:31 +0100 Subject: [PATCH 58/87] fix custom code node size --- Source/Editor/Surface/Archetypes/Material.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 719553651..d9890f031 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -317,6 +317,7 @@ namespace FlaxEditor.Surface.Archetypes { pos += new Float2(65, 0); size = new Float2(160, 185); + _sizeMin = new Float2(240, 185); } else { @@ -969,7 +970,6 @@ namespace FlaxEditor.Surface.Archetypes Title = "Custom Global Code", Description = "Custom global HLSL shader code expression (placed before material shader code). Can contain includes to shader utilities or declare functions to reuse later.", Flags = NodeFlags.MaterialGraph, - UseFixedSize = true, Size = new Float2(300, 240), DefaultValues = new object[] { From bda0dee371248803657ebc815b114988a46f6520 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 28 Mar 2026 22:32:55 +0100 Subject: [PATCH 59/87] fix curve node --- Source/Editor/Surface/Archetypes/Tools.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 757c9f5c9..ee335d01d 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -467,7 +467,6 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new CurveNode(id, context, arch, groupArch), Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.", Flags = NodeFlags.AllGraphs, - UseFixedSize = true, Size = new Float2(400, 180.0f), DefaultValues = new object[] { @@ -516,13 +515,12 @@ namespace FlaxEditor.Surface.Archetypes base.OnLoaded(action); // Create curve editor - var upperLeft = GetBox(0).BottomLeft; - var upperRight = GetBox(1).BottomRight; - float curveMargin = 20.0f; + var upperLeft = GetBox(0).BottomRight; + var upperRight = GetBox(1).BottomLeft; _curve = new BezierCurveEditor { MaxKeyframes = 7, - Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f), + Bounds = new Rectangle(upperLeft + new Float2(0f, 10.0f), upperRight.X - upperLeft.X - 8.0f, 135.0f), Parent = this, AnchorMax = Float2.One, }; From f500fcd6fdfb03edb98666e3a12c592b3c4fd2f2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Mar 2026 23:39:43 +0100 Subject: [PATCH 60/87] Optimize actors copy/paste data to use a single JSON for all objects #3945 --- Flax.flaxproj | 2 +- Source/Engine/Level/Actor.cpp | 127 ++++++++++++---------------------- 2 files changed, 44 insertions(+), 85 deletions(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index dc126772a..8692455ad 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 11, "Revision": 0, - "Build": 6809 + "Build": 6810 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 8123142aa..70a71eee2 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1729,7 +1729,6 @@ void WriteObjectToBytes(SceneObject* obj, rapidjson_flax::StringBuffer& buffer, { // Create JSON CompactJsonWriter writer(buffer); - writer.SceneObject(obj); // Write json to output @@ -1737,28 +1736,24 @@ void WriteObjectToBytes(SceneObject* obj, rapidjson_flax::StringBuffer& buffer, output.WriteInt32((int32)buffer.GetSize()); output.WriteBytes((byte*)buffer.GetString(), (int32)buffer.GetSize()); - // Store order in parent. Makes life easier for editor to sync objects order on undo/redo actions. - output.WriteInt32(obj->GetOrderInParent()); - // Reuse string buffer buffer.Clear(); } bool Actor::ToBytes(const Array& actors, MemoryWriteStream& output) { - PROFILE_CPU(); if (actors.IsEmpty()) { // Cannot serialize empty list return true; } + PROFILE_CPU(); + PROFILE_MEM(Level); // Collect object ids that exist in the serialized data to allow references mapping later Array ids(actors.Count()); for (int32 i = 0; i < actors.Count(); i++) { - // By default we collect actors and scripts (they are ManagedObjects recognized by the id) - auto actor = actors[i]; if (!actor) continue; @@ -1766,7 +1761,8 @@ bool Actor::ToBytes(const Array& actors, MemoryWriteStream& output) for (int32 j = 0; j < actor->Scripts.Count(); j++) { const auto script = actor->Scripts[j]; - ids.Add(script->GetID()); + if (script) + ids.Add(script->GetID()); } } @@ -1776,23 +1772,28 @@ bool Actor::ToBytes(const Array& actors, MemoryWriteStream& output) // Serialized objects ids (for references mapping) output.Write(ids); - // Objects data + // Objects data (JSON) rapidjson_flax::StringBuffer buffer; + CompactJsonWriter writer(buffer); + writer.StartArray(); for (int32 i = 0; i < actors.Count(); i++) { Actor* actor = actors[i]; if (!actor) continue; - - WriteObjectToBytes(actor, buffer, output); - + writer.SceneObject(actor); for (int32 j = 0; j < actor->Scripts.Count(); j++) { Script* script = actor->Scripts[j]; - - WriteObjectToBytes(script, buffer, output); + if (!script) + continue; + writer.SceneObject(script); } } + writer.EndArray(); + // TODO: compress json (LZ4) if it's big enough + output.WriteInt32((int32)buffer.GetSize()); + output.WriteBytes((byte*)buffer.GetString(), (int32)buffer.GetSize()); return false; } @@ -1811,8 +1812,8 @@ Array Actor::ToBytes(const Array& actors) bool Actor::FromBytes(const Span& data, Array& output, ISerializeModifier* modifier) { PROFILE_CPU(); + PROFILE_MEM(Level); output.Clear(); - ASSERT(modifier); if (data.Length() <= 0) return true; @@ -1828,15 +1829,34 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM } // Serialized objects ids (for references mapping) +#if 0 Array ids; stream.Read(ids); int32 objectsCount = ids.Count(); +#else + int32 objectsCount; + stream.ReadInt32(&objectsCount); + stream.Move(objectsCount); +#endif if (objectsCount < 0) return true; + // Load objects data (JSON) + int32 bufferSize; + stream.ReadInt32(&bufferSize); + const char* buffer = (const char*)stream.Move(bufferSize); + rapidjson_flax::Document document; + { + PROFILE_CPU_NAMED("Json.Parse"); + document.Parse(buffer, bufferSize); + } + if (document.HasParseError()) + { + Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); + return true; + } + // Prepare - Array order; - order.Resize(objectsCount); modifier->EngineBuild = engineBuild; CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); sceneObjects->Resize(objectsCount); @@ -1844,34 +1864,10 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM // Deserialize objects Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping); - auto startPos = stream.GetPosition(); for (int32 i = 0; i < objectsCount; i++) { - // Buffer - int32 bufferSize; - stream.ReadInt32(&bufferSize); - const char* buffer = (const char*)stream.GetPositionHandle(); - stream.Move(bufferSize); - - // Order in parent - int32 orderInParent; - stream.ReadInt32(&orderInParent); - order[i] = orderInParent; - - // Load JSON - rapidjson_flax::Document document; - { - PROFILE_CPU_NAMED("Json.Parse"); - document.Parse(buffer, bufferSize); - } - if (document.HasParseError()) - { - Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); - return true; - } - - // Create object - auto obj = SceneObjectsFactory::Spawn(context, document); + auto& objData = document[i]; + auto obj = SceneObjectsFactory::Spawn(context, objData); sceneObjects->At(i) = obj; if (obj == nullptr) { @@ -1879,57 +1875,21 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM continue; } obj->RegisterObject(); - - // Add to results Actor* actor = dynamic_cast(obj); if (actor) - { output.Add(actor); - } } - // TODO: optimize this to call json parsing only once per-object instead of twice (spawn + load) - stream.SetPosition(startPos); for (int32 i = 0; i < objectsCount; i++) { - // Buffer - int32 bufferSize; - stream.ReadInt32(&bufferSize); - const char* buffer = (const char*)stream.GetPositionHandle(); - stream.Move(bufferSize); - - // Order in parent - int32 orderInParent; - stream.ReadInt32(&orderInParent); - - // Load JSON - rapidjson_flax::Document document; - { - PROFILE_CPU_NAMED("Json.Parse"); - document.Parse(buffer, bufferSize); - } - if (document.HasParseError()) - { - Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); - return true; - } - - // Deserialize object + auto& objData = document[i]; auto obj = sceneObjects->At(i); if (obj) - SceneObjectsFactory::Deserialize(context, obj, document); + SceneObjectsFactory::Deserialize(context, obj, objData); else - SceneObjectsFactory::HandleObjectDeserializationError(document); + SceneObjectsFactory::HandleObjectDeserializationError(objData); } Scripting::ObjectsLookupIdMapping.Set(nullptr); - // Update objects order - //for (int32 i = 0; i < objectsCount; i++) - { - //SceneObject* obj = sceneObjects->At(i); - // TODO: remove order from saved data? - //obj->SetOrderInParent(order[i]); - } - // Call events (only for parents because they will propagate events down the tree) CollectionPoolCache::ScopeCache parents = ActorsCache::ActorsListCache.Get(); parents->EnsureCapacity(output.Count()); @@ -1949,8 +1909,7 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM } for (int32 i = 0; i < parents->Count(); i++) { - Actor* actor = parents->At(i); - actor->OnTransformChanged(); + parents->At(i)->OnTransformChanged(); } // Initialize actor that are spawned to scene or create managed instanced for others From b8110e9db32fa1e977cef3371da9ccaa7ef2d140 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 00:13:24 +0200 Subject: [PATCH 61/87] Add `=` to text box separators --- Source/Engine/UI/GUI/Common/TextBoxBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 201e91c61..491fbcd94 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -42,6 +42,7 @@ namespace FlaxEngine.GUI '\\', '>', '<', + '=', }; /// From b3aeab777f5629d8d852a5e99bc160da77d0ad1b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 00:13:49 +0200 Subject: [PATCH 62/87] Fix invalid `CanRenderer` when canvas actor is gone #3945 --- Source/Engine/UI/UICanvas.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index df542aacf..4a6bb7ce1 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -61,6 +61,9 @@ namespace FlaxEngine /// public override bool CanRender() { + if (!Canvas) + return false; + // Sync with canvas options Location = Canvas.RenderLocation; Order = Canvas.Order; From 71ff0c6362bad2fc436166216c78b3bb031e1aaa Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 10:13:25 +0200 Subject: [PATCH 63/87] Fix root linkage for prefab instances copy pasted in Editor #3945 --- Source/Engine/Level/Actor.cpp | 38 +++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 70a71eee2..fe896d1ab 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1185,11 +1185,15 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) } else if (!parent && parentId.IsValid()) { + // Skip warning if object was mapped to empty id (intentionally ignored) Guid tmpId; - if (_prefabObjectID.IsValid()) - LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID); - else if (!modifier->IdsMapping.TryGet(parentId, tmpId) || tmpId.IsValid()) // Skip warning if object was mapped to empty id (intentionally ignored) - LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); + if (!modifier->IdsMapping.TryGet(parentId, tmpId) || tmpId.IsValid()) + { + if (_prefabObjectID.IsValid()) + LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID); + else + LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); + } } } } @@ -1862,6 +1866,32 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM sceneObjects->Resize(objectsCount); SceneObjectsFactory::Context context(modifier); + // Fix root linkage for prefab instances (eg. when user duplicates a sub-prefab actor but not a root one) + SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, document, modifier); + SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData); + for (auto& instance : context.Instances) + { + Guid prefabObjectId; + if (!JsonTools::GetGuidIfValid(prefabObjectId, document[instance.RootIndex], "PrefabObjectID")) + continue; + + // Get the original object from prefab + SceneObject* prefabObject = instance.Prefab->GetDefaultInstance(prefabObjectId); + if (prefabObject && prefabObject->GetParent()) + { + // Add empty mapping to parent object in prefab to prevent linking to it + auto prefabObjectParentId = prefabObject->GetParent()->GetPrefabObjectID(); + instance.IdsMapping[prefabObjectParentId] = Guid::Empty; + modifier->IdsMapping[prefabObjectParentId] = Guid::Empty; + Guid nestedPrefabId, nestedPrefabObjectId; + if (instance.Prefab->GetNestedObject(prefabObjectParentId, nestedPrefabId, nestedPrefabObjectId)) + { + instance.IdsMapping[nestedPrefabObjectId] = Guid::Empty; + modifier->IdsMapping[nestedPrefabObjectId] = Guid::Empty; + } + } + } + // Deserialize objects Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping); for (int32 i = 0; i < objectsCount; i++) From 039407b6ee483af2c1ff65042108e1c7c1dbc819 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 10:14:07 +0200 Subject: [PATCH 64/87] Fix broken prefab linkage when duplicating nested prefab instance root #3945 --- Source/Engine/Level/Actor.cpp | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index fe896d1ab..692828359 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -26,6 +26,7 @@ #include "Engine/Scripting/Scripting.h" #include "Engine/Serialization/ISerializeModifier.h" #include "Engine/Serialization/Serialization.h" +#include "Engine/Serialization/JsonTools.h" #include "Engine/Serialization/JsonWriters.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Serialization/MemoryWriteStream.h" @@ -70,6 +71,44 @@ namespace } return result; } + + void LinkPrefab(SceneObject* object, Prefab* linkPrefab) + { + // Find prefab object that is used by this object in a given prefab + Guid currentPrefabId = object->GetPrefabID(), currentObjectId = object->GetPrefabObjectID(); + RETRY: + auto prefab = Content::Load(currentPrefabId); + Guid nestedPrefabId, nestedObjectId; + if (prefab && prefab->GetNestedObject(currentObjectId, nestedPrefabId, nestedObjectId)) + { + auto nestedPrefab = Content::Load(nestedPrefabId); + if (nestedPrefab) + { + auto nestedObject = (Actor*)nestedPrefab->GetDefaultInstance(nestedObjectId); + if (nestedObject && nestedPrefab == linkPrefab) + { + object->LinkPrefab(nestedPrefabId, nestedObjectId); + return; + } + } + + // Try deeper + currentPrefabId = nestedPrefabId; + currentObjectId = nestedObjectId; + goto RETRY; + } + + // Failed to resolve properly object from a given prefab + object->BreakPrefabLink(); + } + + void LinkPrefabRecursive(Actor* actor, Prefab* linkPrefab) + { + for (auto script : actor->Scripts) + LinkPrefab(script, linkPrefab); + for (auto child : actor->Children) + LinkPrefab(child, linkPrefab); + } } Actor::Actor(const SpawnParams& params) @@ -1930,6 +1969,32 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM Actor* actor = parents->At(i); if (actor->HasPrefabLink() && !actor->IsPrefabRoot()) { + // Find a prefab in which that object is a root to establish a new linkage + Guid currentPrefabId = actor->GetPrefabID(), currentObjectId = actor->GetPrefabObjectID(); + RETRY: + auto prefab = Content::Load(currentPrefabId); + Guid nestedPrefabId, nestedObjectId; + if (prefab && prefab->GetNestedObject(currentObjectId, nestedPrefabId, nestedObjectId)) + { + auto nestedPrefab = Content::Load(nestedPrefabId); + if (nestedPrefab) + { + auto nestedObject = (Actor*)nestedPrefab->GetDefaultInstance(nestedObjectId); + if (nestedObject && nestedObject->IsPrefabRoot()) + { + // Change link to the nested prefab + actor->LinkPrefab(nestedPrefabId, nestedObjectId); + LinkPrefabRecursive(actor, nestedPrefab); + continue; + } + } + + // Try deeper + currentPrefabId = nestedPrefabId; + currentObjectId = nestedObjectId; + goto RETRY; + } + actor->BreakPrefabLink(); } } From 408c6d96b1db50f61211e5a803b6dcbcee8f0b07 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 10:14:25 +0200 Subject: [PATCH 65/87] Minor imporvements --- Source/Engine/Level/SceneObject.cpp | 2 +- Source/Engine/Serialization/JsonTools.cpp | 27 ++++++++++++++++++++ Source/Engine/Serialization/JsonTools.h | 31 +++-------------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/Source/Engine/Level/SceneObject.cpp b/Source/Engine/Level/SceneObject.cpp index 1a447af15..f6a14ae5d 100644 --- a/Source/Engine/Level/SceneObject.cpp +++ b/Source/Engine/Level/SceneObject.cpp @@ -61,7 +61,7 @@ void SceneObject::BreakPrefabLink() String SceneObject::GetNamePath(Char separatorChar) const { - Array names; + Array> names; const Actor* a = dynamic_cast(this); if (!a) a = GetParent(); diff --git a/Source/Engine/Serialization/JsonTools.cpp b/Source/Engine/Serialization/JsonTools.cpp index 32d229a08..dd5c73e17 100644 --- a/Source/Engine/Serialization/JsonTools.cpp +++ b/Source/Engine/Serialization/JsonTools.cpp @@ -73,6 +73,13 @@ void ChangeIds(rapidjson_flax::Value& obj, rapidjson_flax::Document& document, c } } +void JsonTools::MergeObjects(Value& target, Value& source, Value::AllocatorType& allocator) +{ + ASSERT(target.IsObject() && source.IsObject()); + for (auto itr = source.MemberBegin(); itr != source.MemberEnd(); ++itr) + target.AddMember(itr->name, itr->value, allocator); +} + void JsonTools::ChangeIds(Document& doc, const Dictionary& mapping) { if (mapping.IsEmpty()) @@ -236,6 +243,14 @@ Plane JsonTools::GetPlane(const Value& value) return result; } +Rectangle JsonTools::GetRectangle(const Value& value) +{ + return Rectangle( + GetVector2(value, "Location", Vector2::Zero), + GetVector2(value, "Size", Vector2::Zero) + ); +} + BoundingSphere JsonTools::GetBoundingSphere(const Value& value) { BoundingSphere result; @@ -367,4 +382,16 @@ CommonValue JsonTools::GetCommonValue(const Value& value) } return result; } + +bool JsonTools::GetGuidIfValid(Guid& result, const Value& node, const char* name) +{ + auto member = node.FindMember(name); + if (member != node.MemberEnd()) + { + result = GetGuid(member->value); + return result.IsValid(); + } + return false; +} + PRAGMA_ENABLE_DEPRECATION_WARNINGS diff --git a/Source/Engine/Serialization/JsonTools.h b/Source/Engine/Serialization/JsonTools.h index 0e807c784..75080ef1b 100644 --- a/Source/Engine/Serialization/JsonTools.h +++ b/Source/Engine/Serialization/JsonTools.h @@ -37,13 +37,7 @@ public: MergeObjects(target, source, target.GetAllocator()); } - static void MergeObjects(Value& target, Value& source, Value::AllocatorType& allocator) - { - ASSERT(target.IsObject() && source.IsObject()); - for (auto itr = source.MemberBegin(); itr != source.MemberEnd(); ++itr) - target.AddMember(itr->name, itr->value, allocator); - } - + static void MergeObjects(Value& target, Value& source, Value::AllocatorType& allocator); static void ChangeIds(Document& doc, const Dictionary& mapping); public: @@ -77,11 +71,9 @@ public: static Float2 GetFloat2(const Value& value); static Float3 GetFloat3(const Value& value); static Float4 GetFloat4(const Value& value); - static Double2 GetDouble2(const Value& value); static Double3 GetDouble3(const Value& value); static Double4 GetDouble4(const Value& value); - static Color GetColor(const Value& value); static Quaternion GetQuaternion(const Value& value); static Ray GetRay(const Value& value); @@ -89,15 +81,7 @@ public: static Transform GetTransform(const Value& value); static void GetTransform(Transform& result, const Value& value); static Plane GetPlane(const Value& value); - - static Rectangle GetRectangle(const Value& value) - { - return Rectangle( - GetVector2(value, "Location", Vector2::Zero), - GetVector2(value, "Size", Vector2::Zero) - ); - } - + static Rectangle GetRectangle(const Value& value); static BoundingSphere GetBoundingSphere(const Value& value); static BoundingBox GetBoundingBox(const Value& value); static Guid GetGuid(const Value& value); @@ -179,16 +163,7 @@ public: return member != node.MemberEnd() ? GetGuid(member->value) : Guid::Empty; } - FORCE_INLINE static bool GetGuidIfValid(Guid& result, const Value& node, const char* name) - { - auto member = node.FindMember(name); - if (member != node.MemberEnd()) - { - result = GetGuid(member->value); - return result.IsValid(); - } - return false; - } + static bool GetGuidIfValid(Guid& result, const Value& node, const char* name); public: FORCE_INLINE static void GetBool(bool& result, const Value& node, const char* name) From 243173df3c97f2016abaa56ccbd371587ec6fae2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 10:14:50 +0200 Subject: [PATCH 66/87] Fix pasted ghost prefab objects when paste target is not defined #3945 --- .../Editor/Undo/Actions/PasteActorsAction.cs | 36 +++++++++++++------ .../Windows/Assets/PrefabWindow.Actions.cs | 11 ++++-- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/Source/Editor/Undo/Actions/PasteActorsAction.cs b/Source/Editor/Undo/Actions/PasteActorsAction.cs index 2fe4e6235..f4dc7b318 100644 --- a/Source/Editor/Undo/Actions/PasteActorsAction.cs +++ b/Source/Editor/Undo/Actions/PasteActorsAction.cs @@ -41,7 +41,7 @@ namespace FlaxEditor.Actions ActionString = name; _pasteParent = pasteParent; - _idsMapping = new Dictionary(objectIds.Length * 4); + _idsMapping = new Dictionary(objectIds.Length); for (int i = 0; i < objectIds.Length; i++) { _idsMapping[objectIds[i]] = Guid.NewGuid(); @@ -72,13 +72,24 @@ namespace FlaxEditor.Actions /// /// Links the broken parent reference (missing parent). By default links the actor to the first scene. /// - /// The actor. - protected virtual void LinkBrokenParentReference(Actor actor) + /// The actor node. + protected virtual void LinkBrokenParentReference(ActorNode actorNode) { // Link to the first scene root if (Level.ScenesCount == 0) throw new Exception("Failed to paste actor with a broken reference. No loaded scenes."); - actor.SetParent(Level.GetScene(0), false); + actorNode.Actor.SetParent(Level.GetScene(0), false); + } + + /// + /// Checks if actor has a broken parent reference. For example, it's linked to the parent that is indie prefab editor while it should be pasted into scene. + /// + /// The actor node. + protected virtual void CheckBrokenParentReference(ActorNode actorNode) + { + // Ensure pasted object ends up on a scene + if (actorNode.Actor.Scene == null) + LinkBrokenParentReference(actorNode); } /// @@ -103,16 +114,13 @@ namespace FlaxEditor.Actions for (int i = 0; i < actors.Length; i++) { var actor = actors[i]; - - // Check if has no parent linked (broken reference eg. old parent not existing) - if (actor.Parent == null) - { - LinkBrokenParentReference(actor); - } - var node = GetNode(actor.ID); if (node is ActorNode actorNode) { + // Check if has no parent linked (broken reference eg. old parent not existing) + if (actor.Parent == null) + LinkBrokenParentReference(actorNode); + nodes.Add(actorNode); } } @@ -136,6 +144,12 @@ namespace FlaxEditor.Actions nodeParents[i].Actor.SetParent(pasteParentNode.Actor, false); } } + else + { + // Sanity check on pasted actor to ensure they end up i na proper context (scene editor or specific prefab editor) + foreach (var node in nodeParents) + CheckBrokenParentReference(node); + } // Store previously looked up names and the results Dictionary foundNamesResults = new(); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs b/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs index e6b71d5e9..32bf18b19 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs @@ -264,10 +264,17 @@ namespace FlaxEditor.Windows.Assets } /// - protected override void LinkBrokenParentReference(Actor actor) + protected override void LinkBrokenParentReference(ActorNode actorNode) { // Link to prefab root - actor.SetParent(_window.Graph.MainActor, false); + actorNode.Actor.SetParent(_window.Graph.MainActor, false); + } + + /// + protected override void CheckBrokenParentReference(ActorNode actorNode) + { + if (actorNode.Actor.Scene != null || actorNode.Root != _window.Graph.Root) + LinkBrokenParentReference(actorNode); } /// From 767854a2af6c101b08287e601cd5ab9fbfd25881 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 10:18:15 +0200 Subject: [PATCH 67/87] Fix shader error when using Position Offset in deformable material #4028 --- Content/Editor/MaterialTemplates/Deformable.shader | 1 - 1 file changed, 1 deletion(-) diff --git a/Content/Editor/MaterialTemplates/Deformable.shader b/Content/Editor/MaterialTemplates/Deformable.shader index acc303e9d..4de948d28 100644 --- a/Content/Editor/MaterialTemplates/Deformable.shader +++ b/Content/Editor/MaterialTemplates/Deformable.shader @@ -337,7 +337,6 @@ VertexOutput VS_SplineModel(ModelInput input) // Apply world position offset per-vertex #if USE_POSITION_OFFSET output.Geometry.WorldPosition += material.PositionOffset; - output.Geometry.PrevWorldPosition += material.PositionOffset; output.Position = mul(float4(output.Geometry.WorldPosition, 1), ViewProjectionMatrix); #endif From bf20f5d2bfd6977e3b70278a1d92f80c2f228043 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 19:54:47 +0200 Subject: [PATCH 68/87] Fix scripting bindings in searching virtual methods to invoke when there is a name and parameter count collision #3649 --- Source/Engine/Scripting/BinaryModule.cpp | 64 +++------- Source/Engine/Scripting/BinaryModule.h | 4 +- Source/Engine/Scripting/ManagedCLR/MClass.h | 27 ++-- Source/Engine/Scripting/ManagedCLR/MUtils.cpp | 47 +++++-- Source/Engine/Scripting/ManagedCLR/MUtils.h | 1 + Source/Engine/Scripting/Runtime/DotNet.cpp | 35 +++++- Source/Engine/Scripting/Runtime/Mono.cpp | 5 + Source/Engine/Scripting/Runtime/None.cpp | 5 + .../Bindings/BindingsGenerator.Cpp.cs | 117 ++++++++++++++++-- 9 files changed, 216 insertions(+), 89 deletions(-) diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index bbcd7de57..1b254a0c6 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -32,6 +32,18 @@ ManagedBinaryModule* GetBinaryModuleCorlib() #endif } +MMethod* MClass::FindMethod(const char* name, int32 numParams, bool checkBaseClasses) const +{ + MMethod* method = GetMethod(name, numParams); + if (!method && checkBaseClasses) + { + MClass* base = GetBaseClass(); + if (base) + method = base->FindMethod(name, numParams, true); + } + return method; +} + ScriptingTypeHandle::ScriptingTypeHandle(const ScriptingTypeInitializer& initializer) : Module(initializer.Module) , TypeIndex(initializer.TypeIndex) @@ -835,61 +847,17 @@ namespace } return nullptr; } - - bool VariantTypeEquals(const VariantType& type, MType* mType, bool isOut = false) - { - MClass* mClass = MCore::Type::GetClass(mType); - MClass* variantClass = MUtils::GetClass(type); - if (variantClass != mClass) - { - // Hack for Vector2/3/4 which alias with Float2/3/4 or Double2/3/4 (depending on USE_LARGE_WORLDS) - const auto& stdTypes = *StdTypesContainer::Instance(); - if (mClass == stdTypes.Vector2Class && (type.Type == VariantType::Float2 || type.Type == VariantType::Double2)) - return true; - if (mClass == stdTypes.Vector3Class && (type.Type == VariantType::Float3 || type.Type == VariantType::Double3)) - return true; - if (mClass == stdTypes.Vector4Class && (type.Type == VariantType::Float4 || type.Type == VariantType::Double4)) - return true; - - return false; - } - return true; - } } #endif -MMethod* ManagedBinaryModule::FindMethod(MClass* mclass, const ScriptingTypeMethodSignature& signature) +MMethod* ManagedBinaryModule::FindMethod(const MClass* mclass, const ScriptingTypeMethodSignature& signature) { #if USE_CSHARP - if (!mclass) - return nullptr; - const auto& methods = mclass->GetMethods(); - for (MMethod* method : methods) - { - if (method->IsStatic() != signature.IsStatic) - continue; - if (method->GetName() != signature.Name) - continue; - if (method->GetParametersCount() != signature.Params.Count()) - continue; - bool isValid = true; - for (int32 paramIdx = 0; paramIdx < signature.Params.Count(); paramIdx++) - { - auto& param = signature.Params[paramIdx]; - MType* type = method->GetParameterType(paramIdx); - if (param.IsOut != method->GetParameterIsOut(paramIdx) || - !VariantTypeEquals(param.Type, type, param.IsOut)) - { - isValid = false; - break; - } - } - if (isValid && VariantTypeEquals(signature.ReturnType, method->GetReturnType())) - return method; - } -#endif + return mclass ? mclass->GetMethod(signature) : nullptr; +#else return nullptr; +#endif } #if USE_CSHARP diff --git a/Source/Engine/Scripting/BinaryModule.h b/Source/Engine/Scripting/BinaryModule.h index 1da35401b..db346b13c 100644 --- a/Source/Engine/Scripting/BinaryModule.h +++ b/Source/Engine/Scripting/BinaryModule.h @@ -23,7 +23,7 @@ struct ScriptingTypeMethodSignature StringAnsiView Name; VariantType ReturnType; - bool IsStatic; + bool IsStatic = false; Array> Params; }; @@ -322,7 +322,7 @@ public: #endif static ScriptingObject* ManagedObjectSpawn(const ScriptingObjectSpawnParams& params); - static MMethod* FindMethod(MClass* mclass, const ScriptingTypeMethodSignature& signature); + static MMethod* FindMethod(const MClass* mclass, const ScriptingTypeMethodSignature& signature); #if USE_CSHARP static ManagedBinaryModule* FindModule(const MClass* klass); static ScriptingTypeHandle FindType(const MClass* klass); diff --git a/Source/Engine/Scripting/ManagedCLR/MClass.h b/Source/Engine/Scripting/ManagedCLR/MClass.h index 61273dae7..ea26451c1 100644 --- a/Source/Engine/Scripting/ManagedCLR/MClass.h +++ b/Source/Engine/Scripting/ManagedCLR/MClass.h @@ -188,11 +188,11 @@ public: MClass* GetBaseClass() const; /// - /// Checks if this class is a sub class of the specified class (including any derived types). + /// Checks if this class is a subclass of the specified class (including any derived types). /// /// The class. /// True if check interfaces, otherwise just base class. - /// True if this class is a sub class of the specified class. + /// True if this class is a subclass of the specified class. bool IsSubClassOf(const MClass* klass, bool checkInterfaces = false) const; /// @@ -206,7 +206,7 @@ public: /// Checks is the provided object instance of this class' type. /// /// The object to check. - /// True if object is an instance the this class. + /// True if object is an instance this class. bool IsInstanceOfType(MObject* object) const; /// @@ -227,17 +227,7 @@ public: /// The method parameters count. /// True if check base classes when searching for the given method. /// The method or null if failed to find it. - MMethod* FindMethod(const char* name, int32 numParams, bool checkBaseClasses = true) const - { - MMethod* method = GetMethod(name, numParams); - if (!method && checkBaseClasses) - { - MClass* base = GetBaseClass(); - if (base) - method = base->FindMethod(name, numParams, true); - } - return method; - } + MMethod* FindMethod(const char* name, int32 numParams, bool checkBaseClasses = true) const; /// /// Returns an object referencing a method with the specified name and number of parameters. @@ -248,6 +238,13 @@ public: /// The method or null if failed to get it. MMethod* GetMethod(const char* name, int32 numParams = 0) const; + /// + /// Returns an object referencing a method with the specified signature. + /// + /// The method signature. + /// The method or null if failed to get it. + MMethod* GetMethod(const struct ScriptingTypeMethodSignature& signature) const; + /// /// Returns all methods belonging to this class. /// @@ -271,7 +268,7 @@ public: const Array& GetFields() const; /// - /// Returns an object referencing a event with the specified name. + /// Returns an object referencing an event with the specified name. /// /// The event name. /// The event object. diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp index 69fb5aa08..0d7617e0d 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp @@ -845,7 +845,6 @@ MClass* MUtils::GetClass(const VariantType& value) auto mclass = Scripting::FindClass(StringAnsiView(value.TypeName)); if (mclass) return mclass; - const auto& stdTypes = *StdTypesContainer::Instance(); switch (value.Type) { case VariantType::Void: @@ -891,25 +890,25 @@ MClass* MUtils::GetClass(const VariantType& value) case VariantType::Double4: return Double4::TypeInitializer.GetClass(); case VariantType::Color: - return stdTypes.ColorClass; + return Color::TypeInitializer.GetClass(); case VariantType::Guid: - return stdTypes.GuidClass; + return GetBinaryModuleCorlib()->Assembly->GetClass("System.Guid"); case VariantType::Typename: - return stdTypes.TypeClass; + return GetBinaryModuleCorlib()->Assembly->GetClass("System.Type"); case VariantType::BoundingBox: - return stdTypes.BoundingBoxClass; + return BoundingBox::TypeInitializer.GetClass(); case VariantType::BoundingSphere: - return stdTypes.BoundingSphereClass; + return BoundingSphere::TypeInitializer.GetClass(); case VariantType::Quaternion: - return stdTypes.QuaternionClass; + return Quaternion::TypeInitializer.GetClass(); case VariantType::Transform: - return stdTypes.TransformClass; + return Transform::TypeInitializer.GetClass(); case VariantType::Rectangle: - return stdTypes.RectangleClass; + return Rectangle::TypeInitializer.GetClass(); case VariantType::Ray: - return stdTypes.RayClass; + return Ray::TypeInitializer.GetClass(); case VariantType::Matrix: - return stdTypes.MatrixClass; + return Matrix::TypeInitializer.GetClass(); case VariantType::Array: if (value.TypeName) { @@ -1202,8 +1201,7 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) if (value.Type.Type != VariantType::Array) return nullptr; MObject* object = BoxVariant(value); - auto typeStr = MCore::Type::ToString(type); - if (object && !MCore::Object::GetClass(object)->IsSubClassOf(MCore::Type::GetClass(type))) + if (object && MCore::Type::GetClass(type) != MCore::Array::GetArrayClass((MArray*)object)) object = nullptr; return object; } @@ -1238,6 +1236,29 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) return nullptr; } +bool MUtils::VariantTypeEquals(const VariantType& type, MType* mType, bool isOut) +{ + MClass* mClass = MCore::Type::GetClass(mType); + MClass* variantClass = MUtils::GetClass(type); + if (variantClass != mClass) + { + // Hack for Vector2/3/4 which alias with Float2/3/4 or Double2/3/4 (depending on USE_LARGE_WORLDS) + if (mClass->GetFullName() == StringAnsiView("FlaxEngine.Vector2", 18) && (type.Type == VariantType::Float2 || type.Type == VariantType::Double2)) + return true; + if (mClass->GetFullName() == StringAnsiView("FlaxEngine.Vector3", 18) && (type.Type == VariantType::Float3 || type.Type == VariantType::Double3)) + return true; + if (mClass->GetFullName() == StringAnsiView("FlaxEngine.Vector4", 18) && (type.Type == VariantType::Float4 || type.Type == VariantType::Double4)) + return true; + + // Arrays + if (type == VariantType::Array && type.GetElementType() == VariantType::Object) + return MCore::Type::GetType(mType) == MTypes::Array; + + return false; + } + return true; +} + MObject* MUtils::ToManaged(const Version& value) { #if USE_NETCORE diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.h b/Source/Engine/Scripting/ManagedCLR/MUtils.h index f642681d2..5598dbee0 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.h +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.h @@ -621,6 +621,7 @@ namespace MUtils #endif extern void* VariantToManagedArgPtr(Variant& value, MType* type, bool& failed); + extern bool VariantTypeEquals(const VariantType& type, MType* mType, bool isOut = false); extern MObject* ToManaged(const Version& value); extern Version ToNative(MObject* value); diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 4be0ce1a1..21d63e9d6 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -1062,10 +1062,39 @@ MClass* MClass::GetElementClass() const MMethod* MClass::GetMethod(const char* name, int32 numParams) const { GetMethods(); - for (int32 i = 0; i < _methods.Count(); i++) + for (MMethod* method : _methods) { - if (_methods[i]->GetParametersCount() == numParams && _methods[i]->GetName() == name) - return _methods[i]; + if (method->GetParametersCount() == numParams && method->GetName() == name) + return method; + } + return nullptr; +} + +MMethod* MClass::GetMethod(const ScriptingTypeMethodSignature& signature) const +{ + GetMethods(); + for (MMethod* method : _methods) + { + if (method->IsStatic() != signature.IsStatic) + continue; + if (method->GetName() != signature.Name) + continue; + if (method->GetParametersCount() != signature.Params.Count()) + continue; + bool isValid = true; + for (int32 paramIdx = 0; paramIdx < signature.Params.Count(); paramIdx++) + { + auto& param = signature.Params[paramIdx]; + MType* type = method->GetParameterType(paramIdx); + if (param.IsOut != method->GetParameterIsOut(paramIdx) || + !MUtils::VariantTypeEquals(param.Type, type, param.IsOut)) + { + isValid = false; + break; + } + } + if (isValid && (signature.ReturnType.Type == VariantType::Null || MUtils::VariantTypeEquals(signature.ReturnType, method->GetReturnType()))) + return method; } return nullptr; } diff --git a/Source/Engine/Scripting/Runtime/Mono.cpp b/Source/Engine/Scripting/Runtime/Mono.cpp index 06392f932..1449d405f 100644 --- a/Source/Engine/Scripting/Runtime/Mono.cpp +++ b/Source/Engine/Scripting/Runtime/Mono.cpp @@ -1357,6 +1357,11 @@ MMethod* MClass::GetMethod(const char* name, int32 numParams) const return method; } +MMethod* MClass::GetMethod(const ScriptingTypeMethodSignature& signature) const +{ + return GetMethod(signature.Name.Get(), signature.Params.Count()); +} + const Array& MClass::GetMethods() const { if (_hasCachedMethods) diff --git a/Source/Engine/Scripting/Runtime/None.cpp b/Source/Engine/Scripting/Runtime/None.cpp index 1ddaeae8e..178e98444 100644 --- a/Source/Engine/Scripting/Runtime/None.cpp +++ b/Source/Engine/Scripting/Runtime/None.cpp @@ -363,6 +363,11 @@ MMethod* MClass::GetMethod(const char* name, int32 numParams) const return nullptr; } +MMethod* MClass::GetMethod(const ScriptingTypeMethodSignature& signature) const +{ + return nullptr; +} + const Array& MClass::GetMethods() const { _hasCachedMethods = true; diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 4dbbe576c..50d3aa85c 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -215,6 +215,81 @@ namespace Flax.Build.Bindings return $"Variant({value})"; } + public static string GenerateCppWrapperNativeToVariantType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller) + { + // In-built types + switch (typeInfo.Type) + { + case "void": return "VariantType(VariantType::Void)"; + case "bool": return "VariantType(VariantType::Bool)"; + case "int": + case "int32": return "VariantType(VariantType::Int)"; + case "uint": + case "uint32": return "VariantType(VariantType::Uint)"; + case "int64": return "VariantType(VariantType::Int64)"; + case "uint64": return "VariantType(VariantType::Uint64)"; + case "Real": + case "float": return "VariantType(VariantType::Float)"; + case "double": return "VariantType(VariantType::Double)"; + case "StringAnsiView": + case "StringAnsi": + case "StringView": + case "String": return "VariantType(VariantType::String)"; + case "Guid": return "VariantType(VariantType::Guid)"; + case "Asset": return "VariantType(VariantType::Asset)"; + case "Float2": return "VariantType(VariantType::Float2)"; + case "Float3": return "VariantType(VariantType::Float3)"; + case "Float4": return "VariantType(VariantType::Float4)"; + case "Double2": return "VariantType(VariantType::Double2)"; + case "Double3": return "VariantType(VariantType::Double3)"; + case "Double4": return "VariantType(VariantType::Double4)"; + case "Vector2": return "VariantType(VariantType::Vector2)"; + case "Vector3": return "VariantType(VariantType::Vector3)"; + case "Vector4": return "VariantType(VariantType::Vector4)"; + case "Int2": return "VariantType(VariantType::Int2)"; + case "Int3": return "VariantType(VariantType::Int3)"; + case "Int4": return "VariantType(VariantType::Int4)"; + case "Color": return "VariantType(VariantType::Color)"; + case "BoundingBox": return "VariantType(VariantType::BoundingBox)"; + case "BoundingSphere": return "VariantType(VariantType::BoundingSphere)"; + case "Quaternion": return "VariantType(VariantType::Quaternion)"; + case "Transform": return "VariantType(VariantType::Transform)"; + case "Rectangle": return "VariantType(VariantType::Rectangle)"; + case "Ray": return "VariantType(VariantType::Ray)"; + case "Matrix": return "VariantType(VariantType::Matrix)"; + case "Type": return "VariantType(VariantType::Typename)"; + } + + // Array + if (typeInfo.IsArray) + return "VariantType(VariantType::Array)"; + if ((typeInfo.Type == "Array" || typeInfo.Type == "Span") && typeInfo.GenericArgs != null) + { + var elementType = FindApiTypeInfo(buildData, typeInfo.GenericArgs[0], caller); + var elementName = $"{(elementType != null ? elementType.FullNameManaged : typeInfo.GenericArgs[0].Type)}[]"; + return $"VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length}))"; + } + if (typeInfo.Type == "Dictionary" && typeInfo.GenericArgs != null) + return "VariantType(VariantType::Dictionary)"; + + // Scripting type + var apiType = FindApiTypeInfo(buildData, typeInfo, caller); + if (apiType != null) + { + // TODO: optimize VariantType for explicitly defined types to use static name and less mem/allocs + var fullname = apiType.FullNameManaged; + if (apiType.IsEnum) + return $"VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + if (apiType.IsStruct) + return $"VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + if (apiType.IsClass) + return $"VariantType(VariantType::Object, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + } + + // Unknown + return "VariantType()"; + } + public static string GenerateCppWrapperVariantToNative(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, string value) { if (typeInfo.Type == "Variant") @@ -393,7 +468,7 @@ namespace Flax.Build.Bindings return "Scripting::FindClass(\"" + managedType + "\")"; } - private static string GenerateCppGetNativeType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, FunctionInfo functionInfo) + private static string GenerateCppGetNativeType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, FunctionInfo functionInfo = null) { CppIncludeFiles.Add("Engine/Scripting/ManagedCLR/MClass.h"); @@ -1751,7 +1826,23 @@ namespace Flax.Build.Bindings { if (!functionInfo.IsVirtual) continue; - contents.AppendLine($" scriptVTable[{scriptVTableIndex++}] = mclass->GetMethod(\"{functionInfo.Name}\", {functionInfo.Parameters.Count});"); + + // Don't use exact signature for parameter-less methods or the ones without duplicates + if (functionInfo.Parameters.Count == 0 || !classInfo.Functions.Any(x => x != functionInfo && x.Parameters.Count == functionInfo.Parameters.Count && x.Name == functionInfo.Name)) + contents.AppendLine($" scriptVTable[{scriptVTableIndex++}] = mclass->GetMethod(\"{functionInfo.Name}\", {functionInfo.Parameters.Count});"); + else + { + contents.AppendLine(" {"); + contents.AppendLine(" ScriptingTypeMethodSignature signature;"); + contents.AppendLine($" signature.Name = StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length});"); + contents.AppendLine($" signature.Params.Resize({functionInfo.Parameters.Count});"); + for (var i = 0; i < functionInfo.Parameters.Count; i++) + contents.AppendLine($" signature.Params[{i}] = {{ {GenerateCppWrapperNativeToVariantType(buildData, functionInfo.Parameters[i].Type, classInfo)}, {(functionInfo.Parameters[i].IsOut ? "true" : "false")} }};"); + contents.AppendLine($" scriptVTable[{scriptVTableIndex++}] = mclass->GetMethod(signature);"); + if (buildData.Configuration != TargetConfiguration.Release) + contents.AppendLine($" ASSERT(scriptVTable[{scriptVTableIndex - 1}]);"); + contents.AppendLine(" }"); + } } contents.AppendLine(" }"); contents.AppendLine(""); @@ -2672,10 +2763,22 @@ namespace Flax.Build.Bindings { contents.AppendLine(" Variant* parameters = nullptr;"); } + if (functionInfo.Parameters.Count != 0) + { + // Build method signature to find method using exact parameter types to match on name collisions + contents.AppendLine(" ScriptingTypeMethodSignature signature;"); + contents.AppendLine($" signature.Name = StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length});"); + contents.AppendLine($" signature.Params.Resize({functionInfo.Parameters.Count});"); + for (var i = 0; i < functionInfo.Parameters.Count; i++) + contents.AppendLine($" signature.Params[{i}] = {{ parameters[{i}].Type, {(functionInfo.Parameters[i].IsOut ? "true" : "false")} }};"); + } contents.AppendLine(" auto typeHandle = Object->GetTypeHandle();"); contents.AppendLine(" while (typeHandle)"); contents.AppendLine(" {"); - contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});"); + if (functionInfo.Parameters.Count == 0) + contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});"); + else + contents.AppendLine(" auto method = typeHandle.Module->FindMethod(typeHandle, signature);"); contents.AppendLine(" if (method)"); contents.AppendLine(" {"); contents.AppendLine(" Variant __result;"); @@ -2948,7 +3051,9 @@ namespace Flax.Build.Bindings header.Append($"{wrapperName}Array(const {valueType}* v, const int32 length)").AppendLine(); header.Append('{').AppendLine(); header.Append(" Variant result;").AppendLine(); - header.Append(" result.SetType(VariantType(VariantType::Array));").AppendLine(); + var apiType = FindApiTypeInfo(buildData, valueType, moduleInfo); + var elementName = $"{(apiType != null ? apiType.FullNameManaged : valueType.Type)}[]"; + header.Append($" result.SetType(VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length})));").AppendLine(); header.Append(" auto* array = reinterpret_cast*>(result.AsData);").AppendLine(); header.Append(" array->Resize(length);").AppendLine(); header.Append(" for (int32 i = 0; i < length; i++)").AppendLine(); @@ -3316,13 +3421,9 @@ namespace Flax.Build.Bindings contents.AppendLine($"extern \"C\" BinaryModule* GetBinaryModule{binaryModuleName}()"); contents.AppendLine("{"); if (useCSharp) - { contents.AppendLine($" static NativeBinaryModule module(\"{binaryModuleName}\");"); - } else - { contents.AppendLine($" static NativeOnlyBinaryModule module(\"{binaryModuleName}\");"); - } contents.AppendLine(" return &module;"); contents.AppendLine("}"); if (project.VersionControlBranch.Length != 0) From b2457b6ee6032037cdfdefa9908650dd0572cc7f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 19:55:13 +0200 Subject: [PATCH 69/87] Fix Variant array element typename --- Source/Engine/Core/Types/Variant.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 96db06656..2531665b9 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -125,7 +125,7 @@ VariantType::VariantType(Types type, const StringAnsiView& typeName, bool static VariantType::VariantType(Types type, const ScriptingType& sType) : VariantType(type) { - SetTypeName(sType); + SetTypeName(sType.Fullname, sType.Module->CanReload); } VariantType::VariantType(Types type, const MClass* klass) @@ -172,7 +172,7 @@ VariantType::VariantType(const StringAnsiView& typeName) // Check case for array if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive)) { - new(this) VariantType(Array, StringAnsiView(typeName.Get(), typeName.Length() - 2)); + new(this) VariantType(Array, typeName); return; } From 27482161902ac35adf0db30ec4709b169663edf4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 19:55:44 +0200 Subject: [PATCH 70/87] Fix using `Dictionary` as virtual method parameter in scripting bindings --- .../Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 50d3aa85c..8002c66a5 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -997,6 +997,22 @@ namespace Flax.Build.Bindings if (typeInfo.Type == "BytesContainer" && typeInfo.GenericArgs == null) return $"MUtils::ToArray({value})"; + // Dictionary + if (typeInfo.Type == "Dictionary" && typeInfo.GenericArgs != null) + { + CppIncludeFiles.Add("Engine/Scripting/Internal/ManagedDictionary.h"); + var keyClass = GenerateCppGetNativeType(buildData, typeInfo.GenericArgs[0], caller); + var valueClass = GenerateCppGetNativeType(buildData, typeInfo.GenericArgs[1], caller); + return $"ManagedDictionary::ToManaged({value}, {keyClass}, {valueClass})"; + } + + // HashSet + if (typeInfo.Type == "HashSet" && typeInfo.GenericArgs != null) + { + // TODO: automatic converting managed-native for HashSet + throw new NotImplementedException("TODO: converting managed HashSet to native"); + } + // Construct native typename for MUtils template argument var nativeType = new StringBuilder(64); nativeType.Append(typeInfo.Type); From d96e1297f762ec11491fe6952a19231322738285 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 19:56:32 +0200 Subject: [PATCH 71/87] Optimize `MMethod::GetParameterIsOut` to cache all parameters in a single bit-flag --- Source/Engine/Engine/NativeInterop.Unmanaged.cs | 12 +++++++++--- Source/Engine/Scripting/ManagedCLR/MMethod.h | 1 + Source/Engine/Scripting/Runtime/DotNet.cpp | 7 ++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 7af32fc9b..33e102089 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1197,11 +1197,17 @@ namespace FlaxEngine.Interop } [UnmanagedCallersOnly] - internal static byte GetMethodParameterIsOut(ManagedHandle methodHandle, int parameterNum) + internal static UInt64 GetMethodParameterIsOut(ManagedHandle methodHandle) { MethodHolder methodHolder = Unsafe.As(methodHandle.Target); - ParameterInfo parameterInfo = methodHolder.method.GetParameters()[parameterNum]; - return (byte)(parameterInfo.IsOut ? 1 : 0); + var parameters = methodHolder.method.GetParameters(); + UInt64 result = 0; + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].IsOut) + result |= 1ul << i; + } + return result; } [UnmanagedCallersOnly] diff --git a/Source/Engine/Scripting/ManagedCLR/MMethod.h b/Source/Engine/Scripting/ManagedCLR/MMethod.h index 51264d1b8..f6e6b37e2 100644 --- a/Source/Engine/Scripting/ManagedCLR/MMethod.h +++ b/Source/Engine/Scripting/ManagedCLR/MMethod.h @@ -29,6 +29,7 @@ protected: int32 _paramsCount; mutable void* _returnType; mutable Array> _parameterTypes; + mutable uint64 _parameterOuts = 0; void CacheSignature() const; #else StringAnsiView _name; diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 21d63e9d6..65dd09b44 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -1514,13 +1514,16 @@ void MMethod::CacheSignature() const static void* GetMethodReturnTypePtr = GetStaticMethodPointer(TEXT("GetMethodReturnType")); static void* GetMethodParameterTypesPtr = GetStaticMethodPointer(TEXT("GetMethodParameterTypes")); + static void* GetMethodParameterIsOutPtr = GetStaticMethodPointer(TEXT("GetMethodParameterIsOut")); _returnType = CallStaticMethod(GetMethodReturnTypePtr, _handle); + _parameterOuts = 0; if (_paramsCount != 0) { void** parameterTypeHandles; CallStaticMethod(GetMethodParameterTypesPtr, _handle, ¶meterTypeHandles); _parameterTypes.Set(parameterTypeHandles, _paramsCount); MCore::GC::FreeMemory(parameterTypeHandles); + _parameterOuts = CallStaticMethod(GetMethodParameterIsOutPtr, _handle); } _hasCachedSignature = true; @@ -1587,9 +1590,7 @@ bool MMethod::GetParameterIsOut(int32 paramIdx) const if (!_hasCachedSignature) CacheSignature(); ASSERT_LOW_LAYER(paramIdx >= 0 && paramIdx < _paramsCount); - // TODO: cache GetParameterIsOut maybe? - static void* GetMethodParameterIsOutPtr = GetStaticMethodPointer(TEXT("GetMethodParameterIsOut")); - return CallStaticMethod(GetMethodParameterIsOutPtr, _handle, paramIdx); + return _parameterOuts & (1ull << paramIdx); } bool MMethod::HasAttribute(const MClass* klass) const From e2fbd83086bb10278ea134186d589c721c2c5408 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 21:56:01 +0200 Subject: [PATCH 72/87] Optimize `VariantType` name allocs to use static type when possible --- Source/Engine/Scripting/BinaryModule.cpp | 1 - .../Bindings/BindingsGenerator.Cpp.cs | 21 ++++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index 1b254a0c6..02358fac0 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -17,7 +17,6 @@ #include "FlaxEngine.Gen.h" #include "Scripting.h" #include "Events.h" -#include "Internal/StdTypesContainer.h" Dictionary, void(*)(ScriptingObject*, void*, bool)> ScriptingEvents::EventsTable; Delegate, ScriptingTypeHandle, StringView> ScriptingEvents::Event; diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 8002c66a5..eb4ec604f 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -201,13 +201,15 @@ namespace Flax.Build.Bindings if (apiType.IsStruct && !apiType.IsPod && !CppUsedNonPodTypes.Contains(apiType)) CppUsedNonPodTypes.Add(apiType); + var fullname = apiType.FullNameManaged; if (apiType.IsEnum) - return $"Variant::Enum(VariantType(VariantType::Enum, StringAnsiView(\"{apiType.FullNameManaged}\", {apiType.FullNameManaged.Length})), {value})"; + return $"Variant::Enum(VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length})), {value})"; if (apiType.IsStruct && !CppInBuildVariantStructures.Contains(apiType.Name)) - if (typeInfo.IsPtr) - return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{apiType.FullNameManaged}\", {apiType.FullNameManaged.Length})), *{value})"; - else - return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{apiType.FullNameManaged}\", {apiType.FullNameManaged.Length})), {value})"; + { + if (apiType.IsInBuild) + return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length})), {(typeInfo.IsPtr ? "*" + value : value)})"; + return $"Variant::Structure(VariantType(VariantType::Structure, {apiType.FullNameNative}::TypeInitializer.GetType()), {(typeInfo.IsPtr ? "*" + value : value)})"; + } } if (typeInfo.IsPtr && typeInfo.IsConst) @@ -276,14 +278,17 @@ namespace Flax.Build.Bindings var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) { - // TODO: optimize VariantType for explicitly defined types to use static name and less mem/allocs var fullname = apiType.FullNameManaged; if (apiType.IsEnum) return $"VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length}))"; if (apiType.IsStruct) - return $"VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + { + if (apiType.IsInBuild) + return $"VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + return $"VariantType(VariantType::Structure, {apiType.FullNameNative}::TypeInitializer.GetType())"; + } if (apiType.IsClass) - return $"VariantType(VariantType::Object, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + return $"VariantType(VariantType::Object, {apiType.FullNameNative}::TypeInitializer.GetType())"; } // Unknown From 327547a8959a1f8693134a4c850d5c90d8f46f47 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 22:40:37 +0200 Subject: [PATCH 73/87] Rename `FlaxStorage::AllowDataModifications` to `IsReadOnly` --- Source/Engine/Content/BinaryAsset.cpp | 6 ++---- Source/Engine/Content/Cache/AssetsCache.cpp | 4 +--- .../Content/Factories/BinaryAssetFactory.cpp | 2 +- Source/Engine/Content/Storage/FlaxFile.h | 2 +- Source/Engine/Content/Storage/FlaxPackage.h | 2 +- Source/Engine/Content/Storage/FlaxStorage.cpp | 16 ++++++++-------- Source/Engine/Content/Storage/FlaxStorage.h | 4 ++-- 7 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp index 455b6ae3d..eb5aff817 100644 --- a/Source/Engine/Content/BinaryAsset.cpp +++ b/Source/Engine/Content/BinaryAsset.cpp @@ -336,9 +336,7 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool // Force-resolve storage (asset at that path could be not yet loaded into registry) storage = ContentStorageManager::GetStorage(filePath); } - - // Check if can perform write operation to the asset container - if (storage && !storage->AllowDataModifications()) + if (storage && storage->IsReadOnly()) { LOG(Warning, "Cannot write to the asset storage container."); return true; @@ -635,7 +633,7 @@ void BinaryAsset::onRename(const StringView& newPath) ScopeLock lock(Locker); // We don't support packages now - ASSERT(!Storage->IsPackage() && Storage->AllowDataModifications() && Storage->GetEntriesCount() == 1); + ASSERT(!Storage->IsPackage() && !Storage->IsReadOnly() && Storage->GetEntriesCount() == 1); // Rename storage Storage->OnRename(newPath); diff --git a/Source/Engine/Content/Cache/AssetsCache.cpp b/Source/Engine/Content/Cache/AssetsCache.cpp index 2a2d252b6..96083509e 100644 --- a/Source/Engine/Content/Cache/AssetsCache.cpp +++ b/Source/Engine/Content/Cache/AssetsCache.cpp @@ -415,9 +415,7 @@ void AssetsCache::RegisterAssets(FlaxStorage* storage) // Check if need to resolve any collisions if (duplicatedEntries.HasItems()) { - // Check if cannot resolve collision for that container (it must allow to write to) - // TODO: we could support packages as well but don't have to do it now, maybe in future - if (storage->AllowDataModifications() == false) + if (storage->IsReadOnly()) { LOG(Error, "Cannot register \'{0}\'. Founded duplicated asset at \'{1}\' but storage container doesn't allow data modifications.", storagePath, info.Path); return; diff --git a/Source/Engine/Content/Factories/BinaryAssetFactory.cpp b/Source/Engine/Content/Factories/BinaryAssetFactory.cpp index c492bd5b0..9486c6478 100644 --- a/Source/Engine/Content/Factories/BinaryAssetFactory.cpp +++ b/Source/Engine/Content/Factories/BinaryAssetFactory.cpp @@ -28,7 +28,7 @@ bool BinaryAssetFactoryBase::Init(BinaryAsset* asset) #if USE_EDITOR // Check if need to perform data conversion to the newer version (only in Editor) const auto upgrader = GetUpgrader(); - if (storage->AllowDataModifications() && upgrader && upgrader->ShouldUpgrade(initData.SerializedVersion)) + if (!storage->IsReadOnly() && upgrader && upgrader->ShouldUpgrade(initData.SerializedVersion)) { const auto startTime = DateTime::NowUTC(); const AssetInfo info(asset->GetID(), asset->GetTypeName(), storage->GetPath()); diff --git a/Source/Engine/Content/Storage/FlaxFile.h b/Source/Engine/Content/Storage/FlaxFile.h index 1fefd5ae4..387b1b43f 100644 --- a/Source/Engine/Content/Storage/FlaxFile.h +++ b/Source/Engine/Content/Storage/FlaxFile.h @@ -23,7 +23,7 @@ public: // [FlaxStorage] String ToString() const override; bool IsPackage() const override; - bool AllowDataModifications() const override; + bool IsReadOnly() const override; bool HasAsset(const Guid& id) const override; bool HasAsset(const AssetInfo& info) const override; int32 GetEntriesCount() const override; diff --git a/Source/Engine/Content/Storage/FlaxPackage.h b/Source/Engine/Content/Storage/FlaxPackage.h index dee505b74..615a2e8e4 100644 --- a/Source/Engine/Content/Storage/FlaxPackage.h +++ b/Source/Engine/Content/Storage/FlaxPackage.h @@ -24,7 +24,7 @@ public: // [FlaxStorage] String ToString() const override; bool IsPackage() const override; - bool AllowDataModifications() const override; + bool IsReadOnly() const override; bool HasAsset(const Guid& id) const override; bool HasAsset(const AssetInfo& info) const override; int32 GetEntriesCount() const override; diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index e286ee326..5426d7e24 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -809,7 +809,7 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId) // TODO: validate entry ASSERT(newId.IsValid()); - ASSERT(AllowDataModifications()); + ASSERT(!IsReadOnly()); LOG(Info, "Changing asset \'{0}\' id to \'{1}\' (storage: \'{2}\')", e.ID, newId, _path); @@ -883,7 +883,7 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId) FlaxChunk* FlaxStorage::AllocateChunk() { - if (AllowDataModifications()) + if (!IsReadOnly()) { PROFILE_MEM(ContentFiles); auto chunk = New(); @@ -1133,7 +1133,7 @@ bool FlaxStorage::Create(WriteStream* stream, Span assets, const bool FlaxStorage::Save(const AssetInitData& data, bool silentMode) { // Check if can modify the storage - if (!AllowDataModifications()) + if (IsReadOnly()) return true; // Note: we support saving only single asset, to save more assets in single package use FlaxStorage::Create(..) @@ -1544,7 +1544,7 @@ void FlaxStorage::Tick(double time) void FlaxStorage::OnRename(const StringView& newPath) { - ASSERT(AllowDataModifications()); + ASSERT(!IsReadOnly()); _path = newPath; } @@ -1566,9 +1566,9 @@ bool FlaxFile::IsPackage() const return false; } -bool FlaxFile::AllowDataModifications() const +bool FlaxFile::IsReadOnly() const { - return true; + return false; } bool FlaxFile::HasAsset(const Guid& id) const @@ -1646,9 +1646,9 @@ bool FlaxPackage::IsPackage() const return true; } -bool FlaxPackage::AllowDataModifications() const +bool FlaxPackage::IsReadOnly() const { - return false; + return true; } bool FlaxPackage::HasAsset(const Guid& id) const diff --git a/Source/Engine/Content/Storage/FlaxStorage.h b/Source/Engine/Content/Storage/FlaxStorage.h index f01384b68..77ef80ee8 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.h +++ b/Source/Engine/Content/Storage/FlaxStorage.h @@ -276,9 +276,9 @@ public: virtual bool IsPackage() const = 0; /// - /// Checks whenever storage container allows the data modifications. + /// Checks whenever storage container doesn't allow modifications. /// - virtual bool AllowDataModifications() const = 0; + virtual bool IsReadOnly() const = 0; /// /// Determines whether the specified asset exists in this container. From 7b695bf6bf905551e7705b2bbe2ec80425d6eb61 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 22:44:38 +0200 Subject: [PATCH 74/87] Fix compilation regression --- Source/Engine/Level/Actor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 692828359..3f1ab905b 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -20,9 +20,9 @@ #include "Engine/Core/Math/Double4x4.h" #include "Engine/Debug/Exceptions/JsonParseException.h" #include "Engine/Graphics/RenderTask.h" -#include "Engine/Graphics/RenderView.h" #include "Engine/Physics/Physics.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Serialization/ISerializeModifier.h" #include "Engine/Serialization/Serialization.h" From 8b383d4dbe1ff7b92beb21dc78d4356d5833341c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Mar 2026 23:50:03 +0200 Subject: [PATCH 75/87] Fix regression on Large Worlds build and bunch of warnings --- Source/Engine/Core/Types/Variant.h | 2 ++ Source/Engine/Foliage/Foliage.cpp | 2 +- Source/Engine/Level/Actors/Spline.cpp | 2 +- Source/Engine/Renderer/GBufferPass.cpp | 2 +- Source/Engine/Renderer/RenderList.cpp | 2 +- Source/Engine/Tools/ModelTool/ModelTool.cpp | 2 +- Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 5c057bc65..da00d1b62 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -69,10 +69,12 @@ API_STRUCT(InBuild) struct FLAXENGINE_API VariantType MAX, #if USE_LARGE_WORLDS + Real = Double, Vector2 = Double2, Vector3 = Double3, Vector4 = Double4, #else + Real = Float, Vector2 = Float2, Vector3 = Float3, Vector4 = Float4, diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 459f1b662..1fe9e39a9 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -198,7 +198,7 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCal sphere.Center -= context.ViewOrigin; if (Float3::Distance(context.LodView.Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance && context.RenderContext.View.CullingFrustum.Intersects(sphere) && - RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq) + RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, (float)sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq) { const auto modelFrame = instance.DrawState.PrevFrame + 1; diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index d6c2417c8..271609396 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -507,7 +507,7 @@ namespace return; Spline::Keyframe* prev = spline->Curve.GetKeyframes().Get(); Vector3 prevPos = transform.LocalToWorld(prev->Value.Translation); - float distance = Vector3::Distance(prevPos, DebugDraw::GetViewPos()); + Real distance = Vector3::Distance(prevPos, DebugDraw::GetViewPos()); if (distance < METERS_TO_UNITS(800)) // 800m { // Bezier curve diff --git a/Source/Engine/Renderer/GBufferPass.cpp b/Source/Engine/Renderer/GBufferPass.cpp index e62970f83..6b3a655c0 100644 --- a/Source/Engine/Renderer/GBufferPass.cpp +++ b/Source/Engine/Renderer/GBufferPass.cpp @@ -425,7 +425,7 @@ void GBufferPass::DrawSky(RenderContext& renderContext, GPUContext* context) BoundingSphere frustumBounds; renderContext.View.CullingFrustum.GetSphere(frustumBounds); origin = frustumBounds.Center; - size = frustumBounds.Radius; + size = (float)frustumBounds.Radius; } Matrix::Scaling(size / ((float)box.GetSize().Y * 0.5f) * 0.95f, m1); // Scale to fit whole view frustum Matrix::CreateWorld(origin, Float3::Up, Float3::Backward, m2); // Rotate sphere model diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 2bb206d6e..8f28977eb 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -704,7 +704,7 @@ void RenderList::AddDrawCall(const RenderContextBatch& renderContextBatch, DrawP if (drawModes != DrawPass::None && (staticFlags & renderContext.View.StaticFlagsMask) == renderContext.View.StaticFlagsCompare && renderContext.View.CullingFrustum.Intersects(bounds) && - RenderTools::ComputeBoundsScreenRadiusSquared(bounds.Center, bounds.Radius, renderContext.View) * (renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y) >= minObjectPixelSizeSq) + RenderTools::ComputeBoundsScreenRadiusSquared(bounds.Center, (float)bounds.Radius, renderContext.View) * (renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y) >= minObjectPixelSizeSq) { renderContext.List->ShadowDepthDrawCallsList.Indices.Add(index); } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index e3da92da2..977ddb947 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -443,7 +443,7 @@ bool ModelTool::GenerateModelSDF(Model* inputModel, const ModelData* modelData, minDistance *= -1; // Voxel is inside the geometry so turn it into negative distance to the surface const int32 xAddress = x + yAddress; - formatWrite(voxels.Get() + xAddress * formatStride, minDistance * encodeMAD.X + encodeMAD.Y); + formatWrite(voxels.Get() + xAddress * formatStride, (float)minDistance * encodeMAD.X + encodeMAD.Y); } } }; diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index eb4ec604f..3e50670bc 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -230,7 +230,7 @@ namespace Flax.Build.Bindings case "uint32": return "VariantType(VariantType::Uint)"; case "int64": return "VariantType(VariantType::Int64)"; case "uint64": return "VariantType(VariantType::Uint64)"; - case "Real": + case "Real": return "VariantType(VariantType::Real)"; case "float": return "VariantType(VariantType::Float)"; case "double": return "VariantType(VariantType::Double)"; case "StringAnsiView": From c6204fc27427d20bfcd0d5a9d85170f7231801bc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Mar 2026 12:03:18 +0200 Subject: [PATCH 76/87] Fix binary asset dependencies tracking when dependent asset gets loaded later on #3951 Adds additional cache for dependencies tracking to update them when other asset gets loaded later on. Remove critical-section from `LoadAssetTask` that was causing some deadlocks (data access is already atomic there). --- Source/Engine/Content/BinaryAsset.cpp | 5 ++ Source/Engine/Content/BinaryAsset.h | 1 + Source/Engine/Content/Content.cpp | 56 ++++++++++++++++++- Source/Engine/Content/Content.h | 6 ++ .../Content/Loading/Tasks/LoadAssetTask.h | 2 - 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp index eb5aff817..f583ab32a 100644 --- a/Source/Engine/Content/BinaryAsset.cpp +++ b/Source/Engine/Content/BinaryAsset.cpp @@ -84,6 +84,11 @@ bool BinaryAsset::Init(AssetInitData& initData) { asset->_dependantAssets.Add(this); } + else + { + // Dependency is not yet loaded to keep track this link to act when it's loaded + Content::onAssetDepend(this, e.First); + } } #endif diff --git a/Source/Engine/Content/BinaryAsset.h b/Source/Engine/Content/BinaryAsset.h index 39c34588e..e52e9dcff 100644 --- a/Source/Engine/Content/BinaryAsset.h +++ b/Source/Engine/Content/BinaryAsset.h @@ -22,6 +22,7 @@ API_CLASS(Abstract, NoSpawn) class FLAXENGINE_API BinaryAsset : public Asset { DECLARE_ASSET_HEADER(BinaryAsset); + friend Content; protected: AssetHeader _header; FlaxStorageReference _storageRef; // Allow asset to have missing storage reference but only before asset is loaded or if it's virtual diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 59d2dfffa..3ec43ac5a 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -94,6 +94,9 @@ namespace DateTime LastWorkspaceDiscovery; CriticalSection WorkspaceDiscoveryLocker; #endif +#if USE_EDITOR + Dictionary> PendingDependencies; +#endif } #if ENABLE_ASSETS_DISCOVERY @@ -157,6 +160,9 @@ void ContentService::Update() { auto asset = LoadedAssetsToInvoke.Dequeue(); asset->onLoaded_MainThread(); +#if USE_EDITOR + Content::onAddDependencies(asset); +#endif } } @@ -1027,10 +1033,17 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat FileSystem::DeleteFile(tmpPath); // Reload storage - if (auto storage = ContentStorageManager::GetStorage(dstPath, false)) + auto storage = ContentStorageManager::GetStorage(dstPath, false); + if (storage && storage->IsLoaded()) { storage->Reload(); } + else if (auto dependencies = PendingDependencies.TryGet(dstId)) + { + // Destination storage is not loaded but there are other assets that depend on it so update them + for (const auto& e : *dependencies) + e.Item->OnDependencyModified(nullptr); + } } } else @@ -1218,6 +1231,9 @@ void Content::tryCallOnLoaded(Asset* asset) { LoadedAssetsToInvoke.RemoveAtKeepOrder(index); asset->onLoaded_MainThread(); +#if USE_EDITOR + onAddDependencies(asset); +#endif } } @@ -1235,6 +1251,10 @@ void Content::onAssetUnload(Asset* asset) Assets.Remove(asset->GetID()); UnloadQueue.Remove(asset); LoadedAssetsToInvoke.Remove(asset); +#if USE_EDITOR + for (auto& e : PendingDependencies) + e.Value.Remove(asset); +#endif } void Content::onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId) @@ -1242,8 +1262,42 @@ void Content::onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId ScopeLock locker(AssetsLocker); Assets.Remove(oldId); Assets.Add(newId, asset); +#if USE_EDITOR + if (PendingDependencies.ContainsKey(oldId)) + { + auto deps = MoveTemp(PendingDependencies[oldId]); + PendingDependencies.Remove(oldId); + PendingDependencies.Add(newId, MoveTemp(deps)); + } +#endif } +#if USE_EDITOR + +void Content::onAssetDepend(BinaryAsset* asset, const Guid& otherId) +{ + ScopeLock locker(AssetsLocker); + PendingDependencies[otherId].Add(asset); +} + +void Content::onAddDependencies(Asset* asset) +{ + auto it = PendingDependencies.Find(asset->GetID()); + if (it.IsNotEnd()) + { + auto& dependencies = it->Value; + auto binaryAsset = Asset::Cast(asset); + if (binaryAsset) + { + for (const auto& e : dependencies) + binaryAsset->_dependantAssets.Add(e.Item); + } + PendingDependencies.Remove(it); + } +} + +#endif + bool Content::IsAssetTypeIdInvalid(const ScriptingTypeHandle& type, const ScriptingTypeHandle& assetType) { // Skip if no restrictions for the type diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index e286e7c7d..f55f5f9c0 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -389,6 +389,12 @@ private: static void onAssetLoaded(Asset* asset); static void onAssetUnload(Asset* asset); static void onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId); +#if USE_EDITOR + friend class BinaryAsset; + friend class ContentService; + static void onAssetDepend(BinaryAsset* asset, const Guid& otherId); + static void onAddDependencies(Asset* asset); +#endif static void deleteFileSafety(const StringView& path, const Guid& id); // Internal bindings diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h index 4eae2829b..e7d4077fd 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h @@ -80,7 +80,6 @@ private: auto asset = Asset.Get(); if (asset) { - asset->Locker.Lock(); Task* task = (Task*)Platform::AtomicRead(&asset->_loadingTask); if (task) { @@ -99,7 +98,6 @@ private: task = task->GetContinueWithTask(); } while (task); } - asset->Locker.Unlock(); } } }; From b2d1d0f1d966b6a52b5b9b7cc0768d0a8197d797 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Mar 2026 13:13:33 +0200 Subject: [PATCH 77/87] Fix sending replication message to newly connected clients for objects that were not spawned #4029 --- Source/Engine/Networking/NetworkReplicator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index ced964436..2ee14508a 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1863,7 +1863,7 @@ void NetworkInternal::NetworkReplicatorClientConnected(NetworkClient* client) { auto& item = it->Item; ScriptingObject* obj = item.Object.Get(); - if (!obj || !item.Spawned || item.Role != NetworkObjectRole::OwnedAuthoritative) + if (!obj || item.Role != NetworkObjectRole::OwnedAuthoritative) continue; // Mark this client as missing cached data From 917e62621d9620fb5beca45836004ea6777baaff Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Mar 2026 21:45:29 +0200 Subject: [PATCH 78/87] Restore textures background back to Visject surface #3866 --- Source/Editor/Gizmo/UIEditorGizmo.cs | 2 +- Source/Editor/Surface/VisjectSurface.Draw.cs | 31 +++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Source/Editor/Gizmo/UIEditorGizmo.cs b/Source/Editor/Gizmo/UIEditorGizmo.cs index 61109556c..77e6fff9d 100644 --- a/Source/Editor/Gizmo/UIEditorGizmo.cs +++ b/Source/Editor/Gizmo/UIEditorGizmo.cs @@ -529,7 +529,7 @@ namespace FlaxEditor if (EnableBackground && _view != null) { // Draw background - Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Width, Height); + Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Size, _view.Location); if (ShowGrid) { diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index cb7bccae0..5f5078d5f 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -65,8 +65,9 @@ namespace FlaxEditor.Surface /// protected virtual void DrawBackground() { - DrawBackgroundSolidColor(Style.BackgroundColor, Width, Height); - DrawGridBackground(Width, Height); + DrawBackgroundDefault(Style.Background, Size, _rootControl.Location); + //DrawBackgroundSolidColor(Style.BackgroundColor, Width, Height); + DrawGridBackground(); } internal static void DrawBackgroundSolidColor(Color color, float width, float height) @@ -75,7 +76,7 @@ namespace FlaxEditor.Surface Render2D.FillRectangle(backgroundRect, color); } - internal void DrawGridBackground(float width, float height) + internal void DrawGridBackground() { var viewRect = GetClientArea(); var upperLeft = _rootControl.PointFromParent(viewRect.Location); @@ -92,7 +93,7 @@ namespace FlaxEditor.Surface private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange) { var linesColor = Style.BackgroundColor.RGBMultiplied(1.2f); - float[] _gridTickStrengths = { 0f }; + float[] gridTickStrengths = null; Utilities.Utils.DrawCurveTicks((decimal tick, double step, float strength) => { var p = _rootControl.PointToParent(axis * (float)tick); ; @@ -105,31 +106,27 @@ namespace FlaxEditor.Surface ); Render2D.FillRectangle(lineRect, linesColor.AlphaMultiplied(strength)); - }, Utilities.Utils.CurveTickSteps, ref _gridTickStrengths, min, max, pixelRange); + }, Utilities.Utils.CurveTickSteps, ref gridTickStrengths, min, max, pixelRange); } - internal static void DrawBackgroundDefault(Texture background, float width, float height) + internal static void DrawBackgroundDefault(Texture background, Float2 size, Float2 offset) { if (background && background.ResidentMipLevels > 0) { var bSize = background.Size; - float bw = bSize.X; - float bh = bSize.Y; - var pos = Float2.Mod(bSize); + var pos = Float2.Mod(offset / bSize) * bSize; + var max = Float2.Ceil(size / bSize + 1.0f); if (pos.X > 0) - pos.X -= bw; + pos.X -= bSize.X; if (pos.Y > 0) - pos.Y -= bh; + pos.Y -= bSize.Y; - int maxI = Mathf.CeilToInt(width / bw + 1.0f); - int maxJ = Mathf.CeilToInt(height / bh + 1.0f); - - for (int i = 0; i < maxI; i++) + for (int i = 0; i < max.X; i++) { - for (int j = 0; j < maxJ; j++) + for (int j = 0; j < max.Y; j++) { - Render2D.DrawTexture(background, new Rectangle(pos.X + i * bw, pos.Y + j * bh, bw, bh), Color.White); + Render2D.DrawTexture(background, new Rectangle(pos.X + i * bSize.X, pos.Y + j * bSize.Y, bSize), Color.White); } } } From ea5e7f14160626ff6c322e20f2c26938fa8202d1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Mar 2026 21:46:21 +0200 Subject: [PATCH 79/87] Add hover highlight to Visject surface boxes #3866 --- Source/Editor/Surface/SurfaceStyle.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index 4fc656823..2997e1d21 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -244,8 +244,13 @@ namespace FlaxEditor.Surface else icon = hasConnections ? style.Icons.BoxClose : style.Icons.BoxOpen; color *= box.ConnectionsHighlightIntensity + 1; - - // Disable vertex snapping to prevent position jitter/ snapping artefacts for the boxes when zooming the surface + if (box.IsMouseOver) + { + color *= 1.3f; + rect = rect.MakeExpanded(1.0f); + } + + // Disable vertex snapping to prevent position jitter/snapping artefacts for the boxes when zooming the surface var features = Render2D.Features; Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; From ebb4ff1dc2f3e69ea198d425f3b2b5fc4a6e55c5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Mar 2026 22:47:10 +0200 Subject: [PATCH 80/87] Move `UseFixedSize` property from node archetype to flags and optimize unnecessary layouts during node loading #3866 --- Source/Editor/Surface/Archetypes/Animation.cs | 5 ++- Source/Editor/Surface/Archetypes/Particles.cs | 3 +- Source/Editor/Surface/Archetypes/Tools.cs | 6 ++-- Source/Editor/Surface/NodeArchetype.cs | 5 --- Source/Editor/Surface/NodeFlags.cs | 5 +++ Source/Editor/Surface/SurfaceNode.cs | 34 ++++++++----------- 6 files changed, 24 insertions(+), 34 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index c8d45cc5f..9d045185f 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -71,7 +71,7 @@ namespace FlaxEditor.Surface.Archetypes return; _assetSelect.Visible = !box.HasAnyConnection; - if (!Archetype.UseFixedSize) + if (!Archetype.Flags.HasFlag(NodeFlags.FixedSize)) ResizeAuto(); } } @@ -934,8 +934,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 27, Title = "Copy Node", Description = "Copies the skeleton node transformation data (in local space)", - Flags = NodeFlags.AnimGraph, - UseFixedSize = true, + Flags = NodeFlags.AnimGraph | NodeFlags.FixedSize, Size = new Float2(260, 140), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs index d5848a8e4..e0d7f2eb3 100644 --- a/Source/Editor/Surface/Archetypes/Particles.cs +++ b/Source/Editor/Surface/Archetypes/Particles.cs @@ -340,8 +340,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ParticleEmitterNode(id, context, arch, groupArch), Title = "Particle Emitter", Description = "Main particle emitter node. Contains a set of modules per emitter context. Modules are executed in order from top to bottom of the stack.", - Flags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoRemove | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste | NodeFlags.NoCloseButton, - UseFixedSize = true, + Flags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoRemove | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste | NodeFlags.NoCloseButton | NodeFlags.FixedSize, Size = new Float2(300, 600), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index ee335d01d..7e9f5cd9d 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1526,9 +1526,8 @@ namespace FlaxEditor.Surface.Archetypes Title = "Color Gradient", Create = (id, context, arch, groupArch) => new ColorGradientNode(id, context, arch, groupArch), Description = "Linear color gradient sampler", - Flags = NodeFlags.AllGraphs, + Flags = NodeFlags.AllGraphs | NodeFlags.FixedSize, Size = new Float2(400, 150.0f), - UseFixedSize = true, DefaultValues = new object[] { // Stops count @@ -1846,9 +1845,8 @@ namespace FlaxEditor.Surface.Archetypes Title = "Reroute", Create = (id, context, arch, groupArch) => new RerouteNode(id, context, arch, groupArch), Description = "Reroute a connection.", - Flags = NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaGUI | NodeFlags.AllGraphs, + Flags = NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaGUI | NodeFlags.AllGraphs | NodeFlags.FixedSize, Size = RerouteNode.DefaultSize, - UseFixedSize = true, ConnectionsHints = ConnectionsHint.All, IndependentBoxes = new int[] { 0 }, DependentBoxes = new int[] { 1 }, diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 006bd2fb6..b29dd0956 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -129,11 +129,6 @@ namespace FlaxEditor.Surface /// public NodeFlags Flags; - /// - /// If the node should use the as node size. If false, the node will auto resize based on its elements. - /// - public bool UseFixedSize = false; - /// /// Title text. /// diff --git a/Source/Editor/Surface/NodeFlags.cs b/Source/Editor/Surface/NodeFlags.cs index 27900bdb3..4201d9da1 100644 --- a/Source/Editor/Surface/NodeFlags.cs +++ b/Source/Editor/Surface/NodeFlags.cs @@ -78,6 +78,11 @@ namespace FlaxEditor.Surface /// VariableValuesSize = 2048, + /// + /// Node has fixed size defined and should not use automatic layout. + /// + FixedSize = 4096, + /// /// Node can be used in the all visual graphs. /// diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 2d440160f..e1dbeab90 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -348,10 +348,8 @@ namespace FlaxEditor.Surface if (element is Control control) AddChild(control); - if (!Archetype.UseFixedSize) - ResizeAuto(); - else - Resize(Archetype.Size.X, Archetype.Size.Y); + if (!IsLayoutLocked) + UpdateSize(); } /// @@ -912,6 +910,14 @@ namespace FlaxEditor.Surface return sb.ToString(); } + private void UpdateSize() + { + if (Archetype.Flags.HasFlag(NodeFlags.FixedSize)) + Resize(Archetype.Size.X, Archetype.Size.Y); + else + ResizeAuto(); + } + /// protected override bool ShowTooltip => base.ShowTooltip && _headerRect.Contains(ref _mousePosition) && !Surface.IsLeftMouseButtonDown && !Surface.IsRightMouseButtonDown && !Surface.IsPrimaryMenuOpened; @@ -965,10 +971,7 @@ namespace FlaxEditor.Surface box.OnConnectionsChanged(); } - if (!Archetype.UseFixedSize) - ResizeAuto(); - else - Resize(Archetype.Size.X, Archetype.Size.Y); + UpdateSize(); } /// @@ -1007,10 +1010,7 @@ namespace FlaxEditor.Surface _isDuringValuesEditing = false; - if (!Archetype.UseFixedSize) - ResizeAuto(); - else - Resize(Archetype.Size.X, Archetype.Size.Y); + UpdateSize(); } /// @@ -1046,10 +1046,7 @@ namespace FlaxEditor.Surface _isDuringValuesEditing = false; - if (!Archetype.UseFixedSize) - ResizeAuto(); - else - Resize(Archetype.Size.X, Archetype.Size.Y); + UpdateSize(); } internal void SetIsDuringValuesEditing(bool value) @@ -1082,10 +1079,7 @@ namespace FlaxEditor.Surface public virtual void ConnectionTick(Box box) { UpdateBoxesTypes(); - if (!Archetype.UseFixedSize) - ResizeAuto(); - else - Resize(Archetype.Size.X, Archetype.Size.Y); + UpdateSize(); } /// From f1796dc8c8b86454181180e54e7b7cca36b9eddf Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 09:49:29 +0200 Subject: [PATCH 81/87] Fix and improve layout of various Visject nodes and elements #3866 --- Source/Editor/Surface/Archetypes/Animation.cs | 26 +++++++++---------- Source/Editor/Surface/Archetypes/Constants.cs | 2 +- Source/Editor/Surface/Archetypes/Function.cs | 8 +++--- Source/Editor/Surface/Archetypes/Math.cs | 2 +- Source/Editor/Surface/Archetypes/Textures.cs | 6 ++--- Source/Editor/Surface/Archetypes/Tools.cs | 6 ++--- Source/Editor/Surface/Elements/BoolValue.cs | 2 +- Source/Editor/Surface/Elements/ColorValue.cs | 1 + .../Surface/Elements/ComboBoxElement.cs | 1 + Source/Editor/Surface/Elements/EnumValue.cs | 5 ++-- Source/Editor/Surface/Elements/FloatValue.cs | 1 + .../Editor/Surface/Elements/IntegerValue.cs | 1 + .../Surface/Elements/UnsignedIntegerValue.cs | 1 + Source/Editor/Surface/NodeElementArchetype.cs | 2 +- Source/Editor/Surface/SurfaceNode.cs | 16 ++++++------ 15 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index 9d045185f..410f6d33f 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -624,8 +624,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new MultiBlend1D(id, context, arch, groupArch), Title = "Multi Blend 1D", Description = "Animation blending in 1D", - Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize, - Size = new Float2(420, 300), + Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize | NodeFlags.FixedSize, + Size = new Float2(420, 320), DefaultValues = new object[] { // Node data @@ -649,9 +649,9 @@ namespace FlaxEditor.Surface.Archetypes // Axis X NodeElementArchetype.Factory.Input(3, "X", true, typeof(float), 4), - NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY + 2, "(min: max: )"), - NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY + 2, 0), - NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY + 2, 0), + NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY, "(min: max: )"), + NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY, 0), + NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY, 0), } }, new NodeArchetype @@ -660,8 +660,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new MultiBlend2D(id, context, arch, groupArch), Title = "Multi Blend 2D", Description = "Animation blending in 2D", - Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize, - Size = new Float2(420, 620), + Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize | NodeFlags.FixedSize, + Size = new Float2(420, 640), DefaultValues = new object[] { // Node data @@ -685,15 +685,15 @@ namespace FlaxEditor.Surface.Archetypes // Axis X NodeElementArchetype.Factory.Input(3, "X", true, typeof(float), 4), - NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY + 2, "(min: max: )"), - NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY + 2, 0), - NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY + 2, 0), + NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY, "(min: max: )"), + NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY, 0), + NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY, 0), // Axis Y NodeElementArchetype.Factory.Input(4, "Y", true, typeof(float), 5), - NodeElementArchetype.Factory.Text(30, 4 * Surface.Constants.LayoutOffsetY + 2, "(min: max: )"), - NodeElementArchetype.Factory.Vector_Z(60, 4 * Surface.Constants.LayoutOffsetY + 2, 0), - NodeElementArchetype.Factory.Vector_W(145, 4 * Surface.Constants.LayoutOffsetY + 2, 0), + NodeElementArchetype.Factory.Text(30, 4 * Surface.Constants.LayoutOffsetY, "(min: max: )"), + NodeElementArchetype.Factory.Vector_Z(60, 4 * Surface.Constants.LayoutOffsetY, 0), + NodeElementArchetype.Factory.Vector_W(145, 4 * Surface.Constants.LayoutOffsetY, 0), } }, new NodeArchetype diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index a11213f93..19bb767d4 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new EnumComboBox(type) { EnumTypeValue = Values[0], - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, FlaxEditor.Surface.Constants.BoxRowHeight), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.EnumTypeValue); diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 6410b51e7..e3deef368 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -267,7 +267,7 @@ namespace FlaxEditor.Surface.Archetypes { _nameField = new Label { - Width = 140.0f, + Size = new Float2(140, FlaxEditor.Surface.Constants.BoxRowHeight), TextColorHighlighted = Style.Current.ForegroundGrey, HorizontalAlignment = TextAlignment.Near, Parent = this, @@ -386,8 +386,7 @@ namespace FlaxEditor.Surface.Archetypes _types = surface.FunctionTypes; _typePicker = new ComboBox { - Location = new Float2(4, 32), - Width = 80.0f, + Bounds = new Rectangle(4, 34, 80, FlaxEditor.Surface.Constants.BoxRowHeight), Parent = this, }; for (int i = 0; i < _types.Length; i++) @@ -455,8 +454,7 @@ namespace FlaxEditor.Surface.Archetypes _types = surface.FunctionTypes; _typePicker = new ComboBox { - Location = new Float2(24, 32), - Width = 80.0f, + Bounds = new Rectangle(24, 34, 80, FlaxEditor.Surface.Constants.BoxRowHeight), Parent = this, }; for (int i = 0; i < _types.Length; i++) diff --git a/Source/Editor/Surface/Archetypes/Math.cs b/Source/Editor/Surface/Archetypes/Math.cs index 26d271264..9d2ae3bfd 100644 --- a/Source/Editor/Surface/Archetypes/Math.cs +++ b/Source/Editor/Surface/Archetypes/Math.cs @@ -213,7 +213,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 30, Title = "Vector Transform", Description = "Transform vector from source space to destination space", - Flags = NodeFlags.MaterialGraph, + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, Size = new Float2(170, 40), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 2cc56c8a4..929cb153b 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Texture", Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", - Flags = NodeFlags.MaterialGraph, + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, Size = new Float2(140, 140), DefaultValues = new object[] { @@ -176,8 +176,8 @@ namespace FlaxEditor.Surface.Archetypes Title = "Cube Texture", Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", - Flags = NodeFlags.MaterialGraph, - Size = new Float2(140, 120), + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, + Size = new Float2(140, 140), DefaultValues = new object[] { Guid.Empty diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 7e9f5cd9d..c1828284e 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1465,8 +1465,8 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 6, Title = "Panner", Description = "Animates UVs over time", - Flags = NodeFlags.MaterialGraph, - Size = new Float2(170, 80), + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, + Size = new Float2(170, 96), DefaultValues = new object[] { false @@ -1476,7 +1476,7 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), NodeElementArchetype.Factory.Input(1, "Time", true, typeof(float), 1), NodeElementArchetype.Factory.Input(2, "Speed", true, typeof(Float2), 2), - NodeElementArchetype.Factory.Text(18, Surface.Constants.LayoutOffsetY * 3 + 5, "Fractional Part"), + NodeElementArchetype.Factory.Text(20, Surface.Constants.LayoutOffsetY * 3 + 5, "Fractional Part"), NodeElementArchetype.Factory.Bool(0, Surface.Constants.LayoutOffsetY * 3 + 5, 0), NodeElementArchetype.Factory.Output(0, "", typeof(Float2), 3) } diff --git a/Source/Editor/Surface/Elements/BoolValue.cs b/Source/Editor/Surface/Elements/BoolValue.cs index 70ded4dec..457e25f26 100644 --- a/Source/Editor/Surface/Elements/BoolValue.cs +++ b/Source/Editor/Surface/Elements/BoolValue.cs @@ -33,7 +33,7 @@ namespace FlaxEditor.Surface.Elements /// public BoolValue(SurfaceNode parentNode, NodeElementArchetype archetype) - : base(parentNode, archetype, archetype.ActualPosition, new Float2(16), true) + : base(parentNode, archetype, archetype.ActualPosition, new Float2(Constants.BoxRowHeight), true) { } diff --git a/Source/Editor/Surface/Elements/ColorValue.cs b/Source/Editor/Surface/Elements/ColorValue.cs index 671eb74f8..45af92273 100644 --- a/Source/Editor/Surface/Elements/ColorValue.cs +++ b/Source/Editor/Surface/Elements/ColorValue.cs @@ -28,6 +28,7 @@ namespace FlaxEditor.Surface.Elements public ColorValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; UseDynamicEditing = false; diff --git a/Source/Editor/Surface/Elements/ComboBoxElement.cs b/Source/Editor/Surface/Elements/ComboBoxElement.cs index cb1ed79a5..0f06274a4 100644 --- a/Source/Editor/Surface/Elements/ComboBoxElement.cs +++ b/Source/Editor/Surface/Elements/ComboBoxElement.cs @@ -33,6 +33,7 @@ namespace FlaxEditor.Surface.Elements public ComboBoxElement(SurfaceNode parentNode, NodeElementArchetype archetype) : base(archetype.ActualPositionX, archetype.ActualPositionY, archetype.Size.X) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/Elements/EnumValue.cs b/Source/Editor/Surface/Elements/EnumValue.cs index 9aceaeff4..1fe87fef0 100644 --- a/Source/Editor/Surface/Elements/EnumValue.cs +++ b/Source/Editor/Surface/Elements/EnumValue.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI; +using FlaxEngine; using FlaxEngine.Utilities; namespace FlaxEditor.Surface.Elements @@ -29,9 +30,7 @@ namespace FlaxEditor.Surface.Elements public EnumValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(TypeUtils.GetType(archetype.Text).Type) { - X = archetype.ActualPositionX; - Y = archetype.ActualPositionY; - Width = archetype.Size.X; + Bounds = new Rectangle(archetype.ActualPositionX, archetype.ActualPositionY, archetype.Size.X, Constants.BoxRowHeight); ParentNode = parentNode; Archetype = archetype; Value = Convert.ToInt32(ParentNode.Values[Archetype.ValueIndex]); diff --git a/Source/Editor/Surface/Elements/FloatValue.cs b/Source/Editor/Surface/Elements/FloatValue.cs index a90ebb8b4..393145169 100644 --- a/Source/Editor/Surface/Elements/FloatValue.cs +++ b/Source/Editor/Surface/Elements/FloatValue.cs @@ -29,6 +29,7 @@ namespace FlaxEditor.Surface.Elements public FloatValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y, 50, archetype.ValueMin, archetype.ValueMax, 0.01f) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/Elements/IntegerValue.cs b/Source/Editor/Surface/Elements/IntegerValue.cs index dde284211..77f4297ca 100644 --- a/Source/Editor/Surface/Elements/IntegerValue.cs +++ b/Source/Editor/Surface/Elements/IntegerValue.cs @@ -32,6 +32,7 @@ namespace FlaxEditor.Surface.Elements public IntegerValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y, 50, (int)archetype.ValueMin, (int)archetype.ValueMax, 0.05f) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs b/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs index b05a27286..621629819 100644 --- a/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs +++ b/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs @@ -24,6 +24,7 @@ namespace FlaxEditor.Surface.Elements public UnsignedIntegerValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y, 50, (uint)archetype.ValueMin, (uint)archetype.ValueMax, 0.05f) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/NodeElementArchetype.cs b/Source/Editor/Surface/NodeElementArchetype.cs index 0e3c504ff..6db191347 100644 --- a/Source/Editor/Surface/NodeElementArchetype.cs +++ b/Source/Editor/Surface/NodeElementArchetype.cs @@ -506,7 +506,7 @@ namespace FlaxEditor.Surface /// The control height. /// The control tooltip text. /// The archetype. - public static NodeElementArchetype Text(float x, float y, string text, float width = 100.0f, float height = 16.0f, string tooltip = null) + public static NodeElementArchetype Text(float x, float y, string text, float width = 100.0f, float height = 19.0f, string tooltip = null) { return new NodeElementArchetype { diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index e1dbeab90..f1d241485 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -227,27 +227,28 @@ namespace FlaxEditor.Surface var child = Children[i]; if (!child.Visible) continue; + // Input boxes if (child is InputBox inputBox) { - var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 20; + var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 25; if (inputBox.DefaultValueEditor != null) boxWidth += inputBox.DefaultValueEditor.Width + 4; leftWidth = Mathf.Max(leftWidth, boxWidth); - leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + 20.0f); + leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + Constants.BoxRowHeight); } // Output boxes else if (child is OutputBox outputBox) { - rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 20); - rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + 20.0f); + rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 25); + rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + Constants.BoxRowHeight); } // Elements (Float-, int-, uint- value boxes, asset pickers, etc.) // These will only ever be on the left side of the node, so we only adjust left width and height else if (child is SurfaceNodeElementControl elementControl) { leftWidth = Mathf.Max(leftWidth, elementControl.Width + 8f); - leftHeight = Mathf.Max(leftHeight, elementControl.Height + 8f); + leftHeight = Mathf.Max(leftHeight, elementControl.Height); } // Other controls in the node else if (child is Control control) @@ -255,12 +256,12 @@ namespace FlaxEditor.Surface if (control.AnchorPreset == AnchorPresets.TopLeft) { width = Mathf.Max(width, control.Right + 15 + Constants.NodeMarginX); - height = Mathf.Max(height, control.Bottom + 4 - Constants.NodeMarginY - Constants.NodeHeaderHeight); + height = Mathf.Max(height, control.Bottom - Constants.NodeMarginY - Constants.NodeHeaderHeight); } else if (!_headerRect.Intersects(control.Bounds)) { width = Mathf.Max(width, control.Width + 15 + Constants.NodeMarginX); - height = Mathf.Max(height, control.Height + 4); + height = Mathf.Max(height, control.Height); } } } @@ -1099,7 +1100,6 @@ namespace FlaxEditor.Surface public override void Draw() { var style = Style.Current; - var backgroundRect = new Rectangle(Float2.Zero, Size); // Shadow From 9d9d58259886247f6b482bfb859aa00fc1f791ca Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 09:50:02 +0200 Subject: [PATCH 82/87] Add undo to preserve connections when changing parameter in param node --- Source/Editor/Surface/Archetypes/Parameters.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 905f793c9..61d3b3617 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -13,6 +13,7 @@ using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEngine; using FlaxEngine.Utilities; +using FlaxEditor.Surface.Undo; namespace FlaxEditor.Surface.Archetypes { @@ -92,7 +93,17 @@ namespace FlaxEditor.Surface.Archetypes var selected = GetSelected(); var selectedID = selected?.ID ?? Guid.Empty; if (selectedID != (Guid)Values[0]) + { + if (Surface.Undo != null && Surface.Undo.Enabled) + { + // Capture node connections to support undo + var action = new EditNodeConnections(Context, this); + RemoveConnections(); + action.End(); + Surface.AddBatchedUndoAction(action); + } Set(selected, ref selectedID); + } } /// From 7737dbc77f93ce4529e4b41b51d2f73cfde54421 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 15:58:31 +0200 Subject: [PATCH 83/87] Various minor fixes #3866 --- Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs | 5 +++++ Source/Editor/Surface/Archetypes/BehaviorTree.cs | 7 +++---- Source/Editor/Surface/Archetypes/Material.cs | 4 ++-- Source/Editor/Surface/Constants.cs | 2 +- Source/Editor/Surface/VisjectSurface.cs | 2 +- Source/Engine/Core/Math/Rectangle.cs | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index 1d842a7c3..cb8681be0 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -569,6 +569,9 @@ namespace FlaxEditor.Surface.Archetypes // Grid _node.DrawEditorGrid(ref rect); + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + base.Draw(); // Draw debug position @@ -584,6 +587,8 @@ namespace FlaxEditor.Surface.Archetypes Render2D.DrawSprite(icon, debugRect, style.ProgressNormal); } + Render2D.Features = features; + // Frame var frameColor = containsFocus ? style.BackgroundSelected : (IsMouseOver ? style.ForegroundGrey : style.ForegroundDisabled); Render2D.DrawRectangle(new Rectangle(1, 1, rect.Width - 2, rect.Height - 2), frameColor); diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index d99fedaee..cdc223a8d 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -190,7 +190,6 @@ namespace FlaxEditor.Surface.Archetypes public override void Draw() { var style = Style.Current; - var backgroundRect = new Rectangle(Float2.Zero, Size); // Shadow @@ -201,7 +200,7 @@ namespace FlaxEditor.Surface.Archetypes } // Background - Render2D.FillRectangle(backgroundRect, ArchetypeColor); + Render2D.FillRectangle(backgroundRect, BackgroundColor); // Breakpoint hit if (Breakpoint.Hit) @@ -819,10 +818,10 @@ namespace FlaxEditor.Surface.Archetypes { base.OnSurfaceLoaded(action); + var node = Node; if (action == SurfaceNodeActions.Undo) { // Update parent node layout when restoring decorator from undo - var node = Node; if (node != null) { node._decorators = null; @@ -832,7 +831,7 @@ namespace FlaxEditor.Surface.Archetypes else { // Correctly size decorators when surface is loaded - Node.ResizeAuto(); + node?.ResizeAuto(); } } diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index d9890f031..1e93e7a9d 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -932,7 +932,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 36, - Title = "HSVToRGB", + Title = "HSV To RGB", Description = "Converts a HSV value to linear RGB [X = 0/360, Y = 0/1, Z = 0/1]", Flags = NodeFlags.MaterialGraph, Size = new Float2(160, 25), @@ -949,7 +949,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 37, - Title = "RGBToHSV", + Title = "RGB To HSV", Description = "Converts a linear RGB value to HSV [X = 0/360, Y = 0/1, Z = 0/1]", Flags = NodeFlags.MaterialGraph, Size = new Float2(160, 25), diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 474d3979b..eba53cc69 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -46,7 +46,7 @@ namespace FlaxEditor.Surface public const float NodeMarginY = 8.0f; /// - /// The width of the row that is started by a box. + /// The size of the row that is started by a box. /// public const float BoxRowHeight = 19.0f; diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 9f0d78bf6..55c643f12 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -511,7 +511,7 @@ namespace FlaxEditor.Surface { GroupID = Custom.GroupID, Name = "Custom", - Color = Color.Wheat + Color = Color.Wheat.RGBMultiplied(0.4f), }; } else diff --git a/Source/Engine/Core/Math/Rectangle.cs b/Source/Engine/Core/Math/Rectangle.cs index db34f8f83..fcded50ce 100644 --- a/Source/Engine/Core/Math/Rectangle.cs +++ b/Source/Engine/Core/Math/Rectangle.cs @@ -260,7 +260,7 @@ namespace FlaxEngine /// Offseted rectangle. public Rectangle MakeOffsetted(float offset) { - return new Rectangle(Location + new Float2(offset, offset), Size); + return new Rectangle(Location + offset, Size); } /// From dc6d53de7cba69ba93ee98dd5e985217c81efb22 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 16:01:01 +0200 Subject: [PATCH 84/87] Try to fix compile error --- Source/Engine/Content/Content.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index f55f5f9c0..7038edc23 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -12,6 +12,7 @@ class Engine; class FlaxFile; +class BinaryAsset; class IAssetFactory; class AssetsCache; @@ -390,7 +391,7 @@ private: static void onAssetUnload(Asset* asset); static void onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId); #if USE_EDITOR - friend class BinaryAsset; + friend BinaryAsset; friend class ContentService; static void onAssetDepend(BinaryAsset* asset, const Guid& otherId); static void onAddDependencies(Asset* asset); From 42ec33bd9ae37ea70db96451eb413c53351a7510 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 16:25:33 +0200 Subject: [PATCH 85/87] Fix auto-sizing nodes with hidden default value fields (eg, comparison node) #3540 --- Source/Editor/Surface/Elements/Box.cs | 8 ++++---- Source/Editor/Surface/SurfaceNode.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index 4c86af041..a110192cf 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -363,10 +363,10 @@ namespace FlaxEditor.Surface.Elements Assert.AreEqual(r1, r2); // Update - ConnectionTick(); - box.ConnectionTick(); OnConnectionsChanged(); box.OnConnectionsChanged(); + ConnectionTick(); + box.ConnectionTick(); Surface?.OnNodesDisconnected(this, box); } @@ -390,10 +390,10 @@ namespace FlaxEditor.Surface.Elements Assert.IsTrue(AreConnected(box)); // Update - ConnectionTick(); - box.ConnectionTick(); OnConnectionsChanged(); box.OnConnectionsChanged(); + ConnectionTick(); + box.ConnectionTick(); Surface?.OnNodesConnected(this, box); } diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index f1d241485..d28d04e1e 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -232,7 +232,7 @@ namespace FlaxEditor.Surface if (child is InputBox inputBox) { var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 25; - if (inputBox.DefaultValueEditor != null) + if (inputBox.DefaultValueEditor != null && inputBox.DefaultValueEditor.Visible) boxWidth += inputBox.DefaultValueEditor.Width + 4; leftWidth = Mathf.Max(leftWidth, boxWidth); leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + Constants.BoxRowHeight); From 2d0da37907c9b1104563ce196c97635b27fda273 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 16:26:47 +0200 Subject: [PATCH 86/87] Remove `ComparisonNode` from #3540 that is impl in #3866 --- .../Editor/Surface/Archetypes/Comparisons.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index 96a877e32..32baadc1c 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -20,7 +20,6 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = id, Title = title, - Create = (id, context, arch, groupArch) => new ComparisonNode(id, context, arch, groupArch), Description = desc, Flags = NodeFlags.AllGraphs, AlternativeTitles = altTitles, @@ -45,28 +44,6 @@ namespace FlaxEditor.Surface.Archetypes }; } - private class ComparisonNode : SurfaceNode - { - public ComparisonNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) - : base(id, context, nodeArch, groupArch) - { - } - - public override void OnSurfaceLoaded(SurfaceNodeActions action) - { - base.OnSurfaceLoaded(action); - - ResizeAuto(); - } - - public override void ConnectionTick(Box box) - { - base.ConnectionTick(box); - - ResizeAuto(); - } - } - private class SwitchOnEnumNode : SurfaceNode { public SwitchOnEnumNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) @@ -204,7 +181,6 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Switch On Bool", - Create = (id, context, arch, groupArch) => new ComparisonNode(id, context, arch, groupArch), AlternativeTitles = new[] { "if", "switch" }, Description = "Returns one of the input values based on the condition value", Flags = NodeFlags.AllGraphs, From 9a7abd751f122974b3f49d76da62f3637a52c77f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Apr 2026 16:55:28 +0200 Subject: [PATCH 87/87] Fix crash on invalid unpack node usage in shader graph #4030 --- Source/Engine/Visject/ShaderGraph.cpp | 33 ++++++++++----------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/Source/Engine/Visject/ShaderGraph.cpp b/Source/Engine/Visject/ShaderGraph.cpp index faf344263..65e7283b2 100644 --- a/Source/Engine/Visject/ShaderGraph.cpp +++ b/Source/Engine/Visject/ShaderGraph.cpp @@ -493,35 +493,26 @@ void ShaderGenerator::ProcessGroupPacking(Box* box, Node* node, Value& value) // Unpack case 30: { - Box* b = node->GetBox(0); - Value v = tryGetValue(b, Float2::Zero).AsFloat2(); - - int32 subIndex = box->ID - 1; - ASSERT(subIndex >= 0 && subIndex < 2); - - value = Value(ValueType::Float, v.Value + _subs[subIndex]); + value = tryGetValue(node->GetBox(0), Float2::Zero).AsFloat2(); + const int32 subIndex = box->ID - 1; + if (subIndex >= 0 && subIndex < 2) + value = Value(ValueType::Float, value.Value + _subs[subIndex]); break; } case 31: { - Box* b = node->GetBox(0); - Value v = tryGetValue(b, Float3::Zero).AsFloat3(); - - int32 subIndex = box->ID - 1; - ASSERT(subIndex >= 0 && subIndex < 3); - - value = Value(ValueType::Float, v.Value + _subs[subIndex]); + value = tryGetValue(node->GetBox(0), Float3::Zero).AsFloat3(); + const int32 subIndex = box->ID - 1; + if (subIndex >= 0 && subIndex < 3) + value = Value(ValueType::Float, value.Value + _subs[subIndex]); break; } case 32: { - Box* b = node->GetBox(0); - Value v = tryGetValue(b, Float4::Zero).AsFloat4(); - - int32 subIndex = box->ID - 1; - ASSERT(subIndex >= 0 && subIndex < 4); - - value = Value(ValueType::Float, v.Value + _subs[subIndex]); + value = tryGetValue(node->GetBox(0), Float4::Zero).AsFloat4(); + const int32 subIndex = box->ID - 1; + if (subIndex >= 0 && subIndex < 4) + value = Value(ValueType::Float, value.Value + _subs[subIndex]); break; } case 33: