From 968772cbada68a076800bff47323254198dd097f Mon Sep 17 00:00:00 2001 From: Saas Date: Fri, 6 Mar 2026 22:54:17 +0100 Subject: [PATCH 01/51] resize node in all directions --- Source/Editor/Surface/ResizableSurfaceNode.cs | 119 +++++++++++++----- Source/Editor/Surface/SurfaceComment.cs | 16 +-- 2 files changed, 95 insertions(+), 40 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 259c29836..2a1f549fe 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -1,19 +1,22 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Surface { /// - /// Visject Surface node control that cna be resized. + /// Visject Surface node control that can be resized. /// /// [HideInEditor] public class ResizableSurfaceNode : SurfaceNode { + private Float2 _resizeWeight; private Float2 _startResizingSize; - private Float2 _startResizingCornerOffset; + private Float2 _surfaceMouseLocation; + private bool _isMouseOverResizeBorder; /// /// Indicates whether the node is currently being resized. @@ -30,11 +33,6 @@ namespace FlaxEditor.Surface /// protected Float2 _sizeMin = new Float2(240, 160); - /// - /// Node resizing rectangle bounds. - /// - protected Rectangle _resizeButtonRect; - private Float2 SizeValue { get => (Float2)Values[_sizeValueIndex]; @@ -50,7 +48,7 @@ namespace FlaxEditor.Surface /// public override bool CanSelect(ref Float2 location) { - return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); + return base.CanSelect(ref location);// && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); } /// @@ -79,15 +77,9 @@ namespace FlaxEditor.Surface { base.Draw(); - if (Surface.CanEdit) + if (Surface.CanEdit && (_isResizing || _isMouseOverResizeBorder)) { - var style = Style.Current; - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 2f); } } @@ -109,46 +101,120 @@ namespace FlaxEditor.Surface base.OnEndMouseCapture(); } + private bool UpdateIsOverResizeBorder(Float2 location) + { + const float ResizeBorderHalfWidth = 20f; + const float ResizeOnAxisThreshold = 0.85f; + + var nodeRect = new Rectangle(Float2.Zero, Size); + bool onBorder = nodeRect.MakeExpanded(ResizeBorderHalfWidth).Contains(location); + var inNodeRect = nodeRect.MakeExpanded(-ResizeBorderHalfWidth); + bool inNode = inNodeRect.Contains(location); + + Float2 rawResizeWeight = (location - nodeRect.Center) / (nodeRect.Size * 0.5f); + _resizeWeight = new Float2(Mathf.Abs(rawResizeWeight.X) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.X) : 0, + Mathf.Abs(rawResizeWeight.Y) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.Y) : 0); + + _isMouseOverResizeBorder = onBorder && !inNode; + return onBorder && !inNode; + } + + private CursorType ResizeWeightToCursorType() + { + if ((_resizeWeight.X == 1 && _resizeWeight.Y == 0) || (_resizeWeight.X == -1 && _resizeWeight.Y == 0)) + return CursorType.SizeWE; + if ((_resizeWeight.X == 0 && _resizeWeight.Y == 1) || (_resizeWeight.X == 0 && _resizeWeight.Y == -1)) + return CursorType.SizeNS; + if ((_resizeWeight.X == -1 && _resizeWeight.Y == -1) || (_resizeWeight.X == 1 && _resizeWeight.Y == 1)) + return CursorType.SizeNWSE; + if ((_resizeWeight.X == 1 && _resizeWeight.Y == -1) || (_resizeWeight.X == -1 && _resizeWeight.Y == 1)) + return CursorType.SizeNESW; + return CursorType.Default; + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (base.OnMouseDown(location, button)) return true; - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) + if (button == MouseButton.Left && UpdateIsOverResizeBorder(location)) { // Start resizing + UpdateSurfaceMouseLocation(); _isResizing = true; _startResizingSize = Size; - _startResizingCornerOffset = Size - location; StartMouseCapture(); - Cursor = CursorType.SizeNWSE; return true; } return false; } + private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end) + { + var pointOrigin = control.Parent ?? control; + var startPos = pointOrigin.PointFromParent(this, start); + var endPos = pointOrigin.PointFromParent(this, end); + return endPos - startPos; + } + + private void UpdateSurfaceMouseLocation() + { + _surfaceMouseLocation = Surface.PointFromScreen(Input.MouseScreenPosition); + } + /// public override void OnMouseMove(Float2 location) { if (_isResizing) { + var resizeAxisAbs = _resizeWeight.Absolute; + var resizeAxisPos = Float2.Clamp(_resizeWeight, Float2.Zero, Float2.One); + var resizeAxisNeg = Float2.Clamp(-_resizeWeight, Float2.Zero, Float2.One); + + var delta = PointToParent(Surface, location) - _surfaceMouseLocation; + // TODO: scale/size snapping? + delta *= resizeAxisAbs; + + var moveLocation = _surfaceMouseLocation + delta; + + // TODO: Do I need GetControlDelta? + var uiControlDelta = GetControlDelta(this, ref _surfaceMouseLocation, ref moveLocation); var emptySize = CalculateNodeSize(0, 0); - var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin); - Resize(size.X, size.Y); + Location += uiControlDelta * resizeAxisNeg; + Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; + Size = new Float2(Mathf.Max(Size.X, _sizeMin.X), Mathf.Max(Size.Y, _sizeMin.Y)); + SizeValue = Size - emptySize; + SizeValue = new Float2(Mathf.Max(SizeValue.X, _sizeMin.X), Mathf.Max(SizeValue.Y, _sizeMin.Y)); + CalculateNodeSize(Size.X, Size.Y); + UpdateSurfaceMouseLocation(); + } + else if (UpdateIsOverResizeBorder(location)) + { + Cursor = ResizeWeightToCursorType(); } else { + Cursor = CursorType.Default; base.OnMouseMove(location); } } + /// + public override void OnMouseLeave() + { + Cursor = CursorType.Default; + _isMouseOverResizeBorder = false; + base.OnMouseLeave(); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (button == MouseButton.Left && _isResizing) { + Cursor = CursorType.Default; EndResizing(); return true; } @@ -156,19 +222,8 @@ namespace FlaxEditor.Surface return base.OnMouseUp(location, button); } - /// - protected override void UpdateRectangles() - { - base.UpdateRectangles(); - - const float buttonMargin = Constants.NodeCloseButtonMargin; - const float buttonSize = Constants.NodeCloseButtonSize; - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); - } - private void EndResizing() { - Cursor = CursorType.Default; EndMouseCapture(); _isResizing = false; if (_startResizingSize != Size) diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index a76fa245d..aa0b0e811 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -130,7 +130,7 @@ namespace FlaxEditor.Surface _headerRect = new Rectangle(0, 0, Width, headerSize); _closeButtonRect = new Rectangle(Width - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); _colorButtonRect = new Rectangle(_closeButtonRect.Left - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin, buttonSize, buttonSize); + //_resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin, buttonSize, buttonSize); _renameTextBox.Width = Width; _renameTextBox.Height = headerSize; } @@ -189,12 +189,12 @@ namespace FlaxEditor.Surface Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); // Resize button - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + //if (_isResizing) + //{ + // Render2D.FillRectangle(_resizeButtonRect, style.Selection); + // Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); + //} + //Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); } // Selection outline @@ -229,7 +229,7 @@ namespace FlaxEditor.Surface /// public override bool ContainsPoint(ref Float2 location, bool precise) { - return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location); + return _headerRect.Contains(ref location);// || _resizeButtonRect.Contains(ref location); } /// From 2d3d836103f1cea3593750c075c97ed260c93219 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 7 Mar 2026 16:24:05 +0100 Subject: [PATCH 02/51] I need something to diff against --- Source/Editor/Surface/ResizableSurfaceNode.cs | 378 ++++++++++-------- Source/Editor/Surface/SurfaceComment.cs | 2 +- 2 files changed, 217 insertions(+), 163 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 2a1f549fe..ed79c90d2 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -13,15 +13,204 @@ namespace FlaxEditor.Surface [HideInEditor] public class ResizableSurfaceNode : SurfaceNode { - private Float2 _resizeWeight; - private Float2 _startResizingSize; - private Float2 _surfaceMouseLocation; - private bool _isMouseOverResizeBorder; + private class ResizeBorder : Control + { + private const float BorderWidth = 15f; + private const float ResizeOnAxisThreshold = 0.85f; - /// - /// Indicates whether the node is currently being resized. - /// - protected bool _isResizing; + public VisjectSurface Surface; + public ResizableSurfaceNode ResizeableNode; + + private Float2 _surfaceMouseLocation; + private Float2 startResizingSize; + + public bool IsMouseOverResizeBorder { get; private set; } + public bool IsResizing { get; private set; } + + public Action StartResize; + public Action EndResize; + + public Float2 ResizeWeight { get; private set; } + public CursorType CursorType + { + get + { + if ((ResizeWeight.X == 1 && ResizeWeight.Y == 0) || (ResizeWeight.X == -1 && ResizeWeight.Y == 0)) + return CursorType.SizeWE; + if ((ResizeWeight.X == 0 && ResizeWeight.Y == 1) || (ResizeWeight.X == 0 && ResizeWeight.Y == -1)) + return CursorType.SizeNS; + if ((ResizeWeight.X == -1 && ResizeWeight.Y == -1) || (ResizeWeight.X == 1 && ResizeWeight.Y == 1)) + return CursorType.SizeNWSE; + if ((ResizeWeight.X == 1 && ResizeWeight.Y == -1) || (ResizeWeight.X == -1 && ResizeWeight.Y == 1)) + return CursorType.SizeNESW; + + return CursorType.Default; + } + } + + + + public ResizeBorder(VisjectSurface surface, ResizableSurfaceNode resizeableNode) + { + Surface = surface; + ResizeableNode = resizeableNode; + } + + /// + /// Updates location and size to match the resizeable node with the additional padding. + /// + /// The node size. + /// The node location. + public void MatchResizeableNode(Float2 nodeSize, Float2 nodeLocation) + { + Size = nodeSize + new Float2(BorderWidth); + Location = nodeLocation - new Float2(BorderWidth * 0.5f); + } + + private void UpdateSurfaceMouseLocation() + { + _surfaceMouseLocation = Surface.PointFromScreen(Input.MouseScreenPosition); + } + + private void UpdateResizeFlags(Float2 location) + { + var borderRect = Bounds with { Location = Float2.Zero }; + bool onBorder = borderRect.Contains(location); + + Float2 rawResizeWeight = (location - borderRect.Center) / (borderRect.Size * 0.5f); + ResizeWeight = new Float2(Mathf.Abs(rawResizeWeight.X) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.X) : 0, + Mathf.Abs(rawResizeWeight.Y) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.Y) : 0); + + IsMouseOverResizeBorder = onBorder && !ResizeableNode.IsMouseOver; + } + + private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end) + { + var pointOrigin = control.Parent ?? control; + var startPos = pointOrigin.PointFromParent(ResizeableNode, start); + var endPos = pointOrigin.PointFromParent(ResizeableNode, end); + return endPos - startPos; + } + + private void EndResizing() + { + EndMouseCapture(); + IsResizing = false; + if (startResizingSize != ResizeableNode.Size) + { + var emptySize = ResizeableNode.CalculateNodeSize(0, 0); + ResizeableNode.SizeValue = ResizeableNode.Size - emptySize; + Surface.MarkAsEdited(false); + } + } + + public override void OnMouseLeave() + { + Cursor = CursorType.Default; + IsMouseOverResizeBorder = false; + base.OnMouseLeave(); + } + + /// + public override void OnMouseMove(Float2 location) + { + if (!IsResizing) + { + UpdateResizeFlags(location); + } + else + { + var resizeAxisAbs = ResizeWeight.Absolute; + var resizeAxisPos = Float2.Clamp(ResizeWeight, Float2.Zero, Float2.One); + var resizeAxisNeg = Float2.Clamp(-ResizeWeight, Float2.Zero, Float2.One); + + var currentSurfaceMouse = Surface.PointFromScreen(Input.MouseScreenPosition); + var delta = currentSurfaceMouse - _surfaceMouseLocation; + // TODO: scale/size snapping? + delta *= resizeAxisAbs; + + var moveLocation = _surfaceMouseLocation + delta; + + // TODO: Do I need GetControlDelta? + var uiControlDelta = GetControlDelta(this, ref _surfaceMouseLocation, ref moveLocation); + var emptySize = ResizeableNode.CalculateNodeSize(0, 0); + ResizeableNode.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; + Float2 oldSize = ResizeableNode.Size; + ResizeableNode.Size = new Float2(Mathf.Max(ResizeableNode.Size.X, ResizeableNode.sizeMin.X), Mathf.Max(ResizeableNode.Size.Y, ResizeableNode.sizeMin.Y)); + if (oldSize == ResizeableNode.Size) // Only move if size wasn't clamped + { + ResizeableNode.Location += uiControlDelta * resizeAxisNeg; + } + ResizeableNode.SizeValue = ResizeableNode.Size - emptySize; + ResizeableNode.SizeValue = new Float2(Mathf.Max(ResizeableNode.SizeValue.X, ResizeableNode.sizeMin.X), Mathf.Max(ResizeableNode.SizeValue.Y, ResizeableNode.sizeMin.Y)); + ResizeableNode.CalculateNodeSize(ResizeableNode.Size.X, ResizeableNode.Size.Y); + UpdateSurfaceMouseLocation(); + MatchResizeableNode(ResizeableNode.Size, ResizeableNode.Location); + } + + if (IsMouseOverResizeBorder) + Cursor = CursorType; + else + Cursor = CursorType.Default; + + + + base.OnMouseMove(location); + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && IsMouseOverResizeBorder && !IsResizing) + { + // Start resizing + UpdateSurfaceMouseLocation(); + IsResizing = true; + startResizingSize = ResizeableNode.Size; + StartMouseCapture(); + return true; + } + + return base.OnMouseDown(location, button); + } + + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && IsResizing) + { + Cursor = CursorType.Default; + EndResizing(); + return true; + } + + return base.OnMouseUp(location, button); + } + + /// + public override void OnLostFocus() + { + if (IsResizing) + EndResizing(); + + base.OnLostFocus(); + } + + /// + public override void OnEndMouseCapture() + { + if (IsResizing) + EndResizing(); + + base.OnEndMouseCapture(); + } + + //public override void Draw() + //{ + // Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Color.Blue, 1f); + //} + } + + + private ResizeBorder resizeBorder; /// /// Index of the Float2 value in the node values list to store node size. @@ -31,7 +220,7 @@ namespace FlaxEditor.Surface /// /// Minimum node size. /// - protected Float2 _sizeMin = new Float2(240, 160); + protected Float2 sizeMin = new Float2(240, 160); private Float2 SizeValue { @@ -43,12 +232,27 @@ namespace FlaxEditor.Surface public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + CullChildren = false; + ClipChildren = false; + resizeBorder = new ResizeBorder(Surface, this) + { + Parent = Surface.SurfaceRoot, + }; + + resizeBorder.MatchResizeableNode(Size, Location); + } + + /// + protected override void OnLocationChanged() + { + resizeBorder.MatchResizeableNode(Size, Location); + base.OnLocationChanged(); } /// public override bool CanSelect(ref Float2 location) { - return base.CanSelect(ref location);// && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); + return base.CanSelect(ref location); } /// @@ -59,6 +263,7 @@ namespace FlaxEditor.Surface if (Surface != null && Surface.GridSnappingEnabled) size = Surface.SnapToGrid(size, true); Resize(size.X, size.Y); + resizeBorder.MatchResizeableNode(Size, Location); base.OnSurfaceLoaded(action); } @@ -77,161 +282,10 @@ namespace FlaxEditor.Surface { base.Draw(); - if (Surface.CanEdit && (_isResizing || _isMouseOverResizeBorder)) + if (Surface.CanEdit && (resizeBorder.IsResizing || resizeBorder.IsMouseOverResizeBorder)) { Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 2f); } } - - /// - public override void OnLostFocus() - { - if (_isResizing) - EndResizing(); - - base.OnLostFocus(); - } - - /// - public override void OnEndMouseCapture() - { - if (_isResizing) - EndResizing(); - - base.OnEndMouseCapture(); - } - - private bool UpdateIsOverResizeBorder(Float2 location) - { - const float ResizeBorderHalfWidth = 20f; - const float ResizeOnAxisThreshold = 0.85f; - - var nodeRect = new Rectangle(Float2.Zero, Size); - bool onBorder = nodeRect.MakeExpanded(ResizeBorderHalfWidth).Contains(location); - var inNodeRect = nodeRect.MakeExpanded(-ResizeBorderHalfWidth); - bool inNode = inNodeRect.Contains(location); - - Float2 rawResizeWeight = (location - nodeRect.Center) / (nodeRect.Size * 0.5f); - _resizeWeight = new Float2(Mathf.Abs(rawResizeWeight.X) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.X) : 0, - Mathf.Abs(rawResizeWeight.Y) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.Y) : 0); - - _isMouseOverResizeBorder = onBorder && !inNode; - return onBorder && !inNode; - } - - private CursorType ResizeWeightToCursorType() - { - if ((_resizeWeight.X == 1 && _resizeWeight.Y == 0) || (_resizeWeight.X == -1 && _resizeWeight.Y == 0)) - return CursorType.SizeWE; - if ((_resizeWeight.X == 0 && _resizeWeight.Y == 1) || (_resizeWeight.X == 0 && _resizeWeight.Y == -1)) - return CursorType.SizeNS; - if ((_resizeWeight.X == -1 && _resizeWeight.Y == -1) || (_resizeWeight.X == 1 && _resizeWeight.Y == 1)) - return CursorType.SizeNWSE; - if ((_resizeWeight.X == 1 && _resizeWeight.Y == -1) || (_resizeWeight.X == -1 && _resizeWeight.Y == 1)) - return CursorType.SizeNESW; - return CursorType.Default; - } - - /// - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - if (button == MouseButton.Left && UpdateIsOverResizeBorder(location)) - { - // Start resizing - UpdateSurfaceMouseLocation(); - _isResizing = true; - _startResizingSize = Size; - StartMouseCapture(); - return true; - } - - return false; - } - - private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end) - { - var pointOrigin = control.Parent ?? control; - var startPos = pointOrigin.PointFromParent(this, start); - var endPos = pointOrigin.PointFromParent(this, end); - return endPos - startPos; - } - - private void UpdateSurfaceMouseLocation() - { - _surfaceMouseLocation = Surface.PointFromScreen(Input.MouseScreenPosition); - } - - /// - public override void OnMouseMove(Float2 location) - { - if (_isResizing) - { - var resizeAxisAbs = _resizeWeight.Absolute; - var resizeAxisPos = Float2.Clamp(_resizeWeight, Float2.Zero, Float2.One); - var resizeAxisNeg = Float2.Clamp(-_resizeWeight, Float2.Zero, Float2.One); - - var delta = PointToParent(Surface, location) - _surfaceMouseLocation; - // TODO: scale/size snapping? - delta *= resizeAxisAbs; - - var moveLocation = _surfaceMouseLocation + delta; - - // TODO: Do I need GetControlDelta? - var uiControlDelta = GetControlDelta(this, ref _surfaceMouseLocation, ref moveLocation); - var emptySize = CalculateNodeSize(0, 0); - Location += uiControlDelta * resizeAxisNeg; - Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; - Size = new Float2(Mathf.Max(Size.X, _sizeMin.X), Mathf.Max(Size.Y, _sizeMin.Y)); - SizeValue = Size - emptySize; - SizeValue = new Float2(Mathf.Max(SizeValue.X, _sizeMin.X), Mathf.Max(SizeValue.Y, _sizeMin.Y)); - CalculateNodeSize(Size.X, Size.Y); - UpdateSurfaceMouseLocation(); - } - else if (UpdateIsOverResizeBorder(location)) - { - Cursor = ResizeWeightToCursorType(); - } - else - { - Cursor = CursorType.Default; - base.OnMouseMove(location); - } - } - - /// - public override void OnMouseLeave() - { - Cursor = CursorType.Default; - _isMouseOverResizeBorder = false; - base.OnMouseLeave(); - } - - /// - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (button == MouseButton.Left && _isResizing) - { - Cursor = CursorType.Default; - EndResizing(); - return true; - } - - return base.OnMouseUp(location, button); - } - - private void EndResizing() - { - EndMouseCapture(); - _isResizing = false; - if (_startResizingSize != Size) - { - var emptySize = CalculateNodeSize(0, 0); - SizeValue = Size - emptySize; - Surface.MarkAsEdited(false); - } - } } } diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index aa0b0e811..05e89b705 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.NodeHeaderSize); _renameTextBox = new TextBox(false, 0, 0, Width) { Height = Constants.NodeHeaderSize, From 2805c0dd7b9f69ee8bb9d4e6e7b949d3c21a51b8 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 7 Mar 2026 23:48:24 +0100 Subject: [PATCH 03/51] please squash commit history before merge I need sth to diff against again --- Source/Editor/Surface/ResizableSurfaceNode.cs | 167 +++++++++++------- Source/Editor/Surface/SurfaceComment.cs | 13 +- 2 files changed, 103 insertions(+), 77 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index ed79c90d2..6fa7225c5 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -13,58 +13,73 @@ namespace FlaxEditor.Surface [HideInEditor] public class ResizableSurfaceNode : SurfaceNode { - private class ResizeBorder : Control + /// + /// Helper class for that handles mouse interactions resizing the node itself. + /// + protected class ResizeBorder : Control { + /// + /// Distance to each of the 4 node edges that the cursor has to be so the user can resize along the direction of the edge. + /// private const float BorderWidth = 15f; - private const float ResizeOnAxisThreshold = 0.85f; - - public VisjectSurface Surface; - public ResizableSurfaceNode ResizeableNode; + private readonly VisjectSurface Surface; + private readonly ResizableSurfaceNode ResizableNode; private Float2 _surfaceMouseLocation; private Float2 startResizingSize; + /// + /// True if the mouse is at the border of the resizable node and not further away from the border than . + /// public bool IsMouseOverResizeBorder { get; private set; } + + /// + /// True if is being resized. + /// public bool IsResizing { get; private set; } - public Action StartResize; - public Action EndResize; + /// + /// The direction in which to resize the node. Should be either -1, 0 or 1 on both axes. + /// + public Float2 ResizeDirection { get; private set; } - public Float2 ResizeWeight { get; private set; } + /// + /// The type of cursor to show to hint to the user that they can resize the node in a given direction. + /// public CursorType CursorType { get { - if ((ResizeWeight.X == 1 && ResizeWeight.Y == 0) || (ResizeWeight.X == -1 && ResizeWeight.Y == 0)) + if ((ResizeDirection.X == 1 && ResizeDirection.Y == 0) || (ResizeDirection.X == -1 && ResizeDirection.Y == 0)) return CursorType.SizeWE; - if ((ResizeWeight.X == 0 && ResizeWeight.Y == 1) || (ResizeWeight.X == 0 && ResizeWeight.Y == -1)) + if ((ResizeDirection.X == 0 && ResizeDirection.Y == 1) || (ResizeDirection.X == 0 && ResizeDirection.Y == -1)) return CursorType.SizeNS; - if ((ResizeWeight.X == -1 && ResizeWeight.Y == -1) || (ResizeWeight.X == 1 && ResizeWeight.Y == 1)) + if ((ResizeDirection.X == -1 && ResizeDirection.Y == -1) || (ResizeDirection.X == 1 && ResizeDirection.Y == 1)) return CursorType.SizeNWSE; - if ((ResizeWeight.X == 1 && ResizeWeight.Y == -1) || (ResizeWeight.X == -1 && ResizeWeight.Y == 1)) + if ((ResizeDirection.X == 1 && ResizeDirection.Y == -1) || (ResizeDirection.X == -1 && ResizeDirection.Y == 1)) return CursorType.SizeNESW; return CursorType.Default; } } - - public ResizeBorder(VisjectSurface surface, ResizableSurfaceNode resizeableNode) + /// + public ResizeBorder(VisjectSurface surface, ResizableSurfaceNode resizableNode) { Surface = surface; - ResizeableNode = resizeableNode; + ResizableNode = resizableNode; } /// - /// Updates location and size to match the resizeable node with the additional padding. + /// Updates location and size to match the resizable node with the additional padding. /// /// The node size. /// The node location. - public void MatchResizeableNode(Float2 nodeSize, Float2 nodeLocation) + public void MatchResizableNode(Float2 nodeSize, Float2 nodeLocation) { - Size = nodeSize + new Float2(BorderWidth); - Location = nodeLocation - new Float2(BorderWidth * 0.5f); + Size = nodeSize + new Float2(BorderWidth * 2); + Location = nodeLocation - new Float2(BorderWidth); } private void UpdateSurfaceMouseLocation() @@ -72,23 +87,27 @@ namespace FlaxEditor.Surface _surfaceMouseLocation = Surface.PointFromScreen(Input.MouseScreenPosition); } - private void UpdateResizeFlags(Float2 location) + private void UpdateResizeFlags(Float2 mouseLocation) { var borderRect = Bounds with { Location = Float2.Zero }; - bool onBorder = borderRect.Contains(location); + bool onBorder = borderRect.Contains(mouseLocation); + // Check this the way we do because some resizeable nodes (like comments) have an implementation of `Control.ContainsPoint` + // that does not check for the size you would think they have based on their visual appearance + bool inNode = borderRect.MakeExpanded(-BorderWidth * 2f).Contains(mouseLocation); - Float2 rawResizeWeight = (location - borderRect.Center) / (borderRect.Size * 0.5f); - ResizeWeight = new Float2(Mathf.Abs(rawResizeWeight.X) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.X) : 0, - Mathf.Abs(rawResizeWeight.Y) >= ResizeOnAxisThreshold ? Mathf.Sign(rawResizeWeight.Y) : 0); + Float2 rawResizeDirection = (mouseLocation - borderRect.Center); + var nodeHalfSizeNoBorder = ResizableNode.Size * 0.5f - BorderWidth; + ResizeDirection = new Float2(Mathf.Abs(rawResizeDirection.X) >= nodeHalfSizeNoBorder.X ? Mathf.Sign(rawResizeDirection.X) : 0, + Mathf.Abs(rawResizeDirection.Y) >= nodeHalfSizeNoBorder.Y ? Mathf.Sign(rawResizeDirection.Y) : 0); - IsMouseOverResizeBorder = onBorder && !ResizeableNode.IsMouseOver; + IsMouseOverResizeBorder = false;// onBorder && !inNode; } private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end) { var pointOrigin = control.Parent ?? control; - var startPos = pointOrigin.PointFromParent(ResizeableNode, start); - var endPos = pointOrigin.PointFromParent(ResizeableNode, end); + var startPos = pointOrigin.PointFromParent(ResizableNode, start); + var endPos = pointOrigin.PointFromParent(ResizableNode, end); return endPos - startPos; } @@ -96,14 +115,15 @@ namespace FlaxEditor.Surface { EndMouseCapture(); IsResizing = false; - if (startResizingSize != ResizeableNode.Size) + if (startResizingSize != ResizableNode.Size) { - var emptySize = ResizeableNode.CalculateNodeSize(0, 0); - ResizeableNode.SizeValue = ResizeableNode.Size - emptySize; + var emptySize = ResizableNode.CalculateNodeSize(0, 0); + ResizableNode.SizeValue = ResizableNode.Size - emptySize; Surface.MarkAsEdited(false); } } + /// public override void OnMouseLeave() { Cursor = CursorType.Default; @@ -120,44 +140,53 @@ namespace FlaxEditor.Surface } else { - var resizeAxisAbs = ResizeWeight.Absolute; - var resizeAxisPos = Float2.Clamp(ResizeWeight, Float2.Zero, Float2.One); - var resizeAxisNeg = Float2.Clamp(-ResizeWeight, Float2.Zero, Float2.One); + var resizeAxisAbs = ResizeDirection.Absolute; + var resizeAxisPos = Float2.Clamp(ResizeDirection, Float2.Zero, Float2.One); + var resizeAxisNeg = Float2.Clamp(-ResizeDirection, Float2.Zero, Float2.One); var currentSurfaceMouse = Surface.PointFromScreen(Input.MouseScreenPosition); var delta = currentSurfaceMouse - _surfaceMouseLocation; + // TODO: scale/size snapping? delta *= resizeAxisAbs; var moveLocation = _surfaceMouseLocation + delta; - // TODO: Do I need GetControlDelta? var uiControlDelta = GetControlDelta(this, ref _surfaceMouseLocation, ref moveLocation); - var emptySize = ResizeableNode.CalculateNodeSize(0, 0); - ResizeableNode.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; - Float2 oldSize = ResizeableNode.Size; - ResizeableNode.Size = new Float2(Mathf.Max(ResizeableNode.Size.X, ResizeableNode.sizeMin.X), Mathf.Max(ResizeableNode.Size.Y, ResizeableNode.sizeMin.Y)); - if (oldSize == ResizeableNode.Size) // Only move if size wasn't clamped - { - ResizeableNode.Location += uiControlDelta * resizeAxisNeg; - } - ResizeableNode.SizeValue = ResizeableNode.Size - emptySize; - ResizeableNode.SizeValue = new Float2(Mathf.Max(ResizeableNode.SizeValue.X, ResizeableNode.sizeMin.X), Mathf.Max(ResizeableNode.SizeValue.Y, ResizeableNode.sizeMin.Y)); - ResizeableNode.CalculateNodeSize(ResizeableNode.Size.X, ResizeableNode.Size.Y); + var emptySize = ResizableNode.CalculateNodeSize(0, 0); + + + // TODO: Fix: Can't move comments anymore + + // TODO: If resize is blocked by min size and the user tries to increase the size again, wait until !blocked by min size to apply delta again + // To do this, just record pos when starting to block by min size and if (cursorLocation > min) { ResizeAgain() } + + ResizableNode.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; + ResizableNode.Size = new Float2(Mathf.Max(ResizableNode.Size.X, ResizableNode.sizeMin.X), Mathf.Max(ResizableNode.Size.Y, ResizableNode.sizeMin.Y)); + ResizableNode.Location += uiControlDelta * resizeAxisNeg; + + // Only move if size wasn't clamped + + + //Debug.Log($"OLD: {oldSize} NEW: {ResizableNode.Size}"); + + ResizableNode.SizeValue = ResizableNode.Size - emptySize; + ResizableNode.SizeValue = new Float2(Mathf.Max(ResizableNode.SizeValue.X, ResizableNode.sizeMin.X), Mathf.Max(ResizableNode.SizeValue.Y, ResizableNode.sizeMin.Y)); + ResizableNode.CalculateNodeSize(ResizableNode.Size.X, ResizableNode.Size.Y); + UpdateSurfaceMouseLocation(); - MatchResizeableNode(ResizeableNode.Size, ResizeableNode.Location); + MatchResizableNode(ResizableNode.Size, ResizableNode.Location); } if (IsMouseOverResizeBorder) Cursor = CursorType; else Cursor = CursorType.Default; - - base.OnMouseMove(location); } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (button == MouseButton.Left && IsMouseOverResizeBorder && !IsResizing) @@ -165,7 +194,7 @@ namespace FlaxEditor.Surface // Start resizing UpdateSurfaceMouseLocation(); IsResizing = true; - startResizingSize = ResizeableNode.Size; + startResizingSize = ResizableNode.Size; StartMouseCapture(); return true; } @@ -173,6 +202,7 @@ namespace FlaxEditor.Surface return base.OnMouseDown(location, button); } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (button == MouseButton.Left && IsResizing) @@ -203,14 +233,17 @@ namespace FlaxEditor.Surface base.OnEndMouseCapture(); } - //public override void Draw() - //{ - // Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Color.Blue, 1f); - //} + /// + public override void Draw() + { + Render2D.DrawRectangle(Bounds with { Location = Float2.Zero }, Color.Green, 1f); + } } - - private ResizeBorder resizeBorder; + /// + /// Represents the border control used for resizing the associated element. + /// + protected ResizeBorder ResizeBorderControl; /// /// Index of the Float2 value in the node values list to store node size. @@ -232,28 +265,26 @@ namespace FlaxEditor.Surface public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { - CullChildren = false; - ClipChildren = false; - resizeBorder = new ResizeBorder(Surface, this) + ResizeBorderControl = new ResizeBorder(Surface, this) { Parent = Surface.SurfaceRoot, }; - resizeBorder.MatchResizeableNode(Size, Location); + ResizeBorderControl.MatchResizableNode(Size, Location); } /// protected override void OnLocationChanged() { - resizeBorder.MatchResizeableNode(Size, Location); + ResizeBorderControl.MatchResizableNode(Size, Location); base.OnLocationChanged(); } - /// - public override bool CanSelect(ref Float2 location) - { - return base.CanSelect(ref location); - } + ///// + //public override bool CanSelect(ref Float2 location) + //{ + // return base.CanSelect(ref location); + //} /// public override void OnSurfaceLoaded(SurfaceNodeActions action) @@ -263,7 +294,7 @@ namespace FlaxEditor.Surface if (Surface != null && Surface.GridSnappingEnabled) size = Surface.SnapToGrid(size, true); Resize(size.X, size.Y); - resizeBorder.MatchResizeableNode(Size, Location); + ResizeBorderControl.MatchResizableNode(Size, Location); base.OnSurfaceLoaded(action); } @@ -282,9 +313,9 @@ namespace FlaxEditor.Surface { base.Draw(); - if (Surface.CanEdit && (resizeBorder.IsResizing || resizeBorder.IsMouseOverResizeBorder)) + if (Surface.CanEdit && (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder)) { - Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 2f); + Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); } } } diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 05e89b705..3a8416fd3 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -130,7 +130,6 @@ namespace FlaxEditor.Surface _headerRect = new Rectangle(0, 0, Width, headerSize); _closeButtonRect = new Rectangle(Width - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); _colorButtonRect = new Rectangle(_closeButtonRect.Left - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); - //_resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin, buttonSize, buttonSize); _renameTextBox.Width = Width; _renameTextBox.Height = headerSize; } @@ -188,13 +187,9 @@ namespace FlaxEditor.Surface // Color button Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - // Resize button - //if (_isResizing) - //{ - // Render2D.FillRectangle(_resizeButtonRect, style.Selection); - // Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - //} - //Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + // Resize + if (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder) + Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); } // Selection outline @@ -229,7 +224,7 @@ namespace FlaxEditor.Surface /// public override bool ContainsPoint(ref Float2 location, bool precise) { - return _headerRect.Contains(ref location);// || _resizeButtonRect.Contains(ref location); + return _headerRect.Contains(ref location); } /// From 1105e58b066819fac0feb8d3bda321d08144a3c5 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 8 Mar 2026 14:53:48 +0100 Subject: [PATCH 04/51] fix not being able to move comments nodes --- Source/Editor/Surface/SurfaceComment.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 3a8416fd3..c257dddd1 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -81,12 +81,16 @@ namespace FlaxEditor.Surface if (Values.Length < 4) { if (IndexInParent > 0) + { IndexInParent = 0; + ResizeBorderControl.IndexInParent = - 1; + } OrderValue = IndexInParent; } else if (OrderValue != -1) { IndexInParent = OrderValue; + ResizeBorderControl.IndexInParent = OrderValue - 1; } } From 2c8ade42888cb2520fc63328bfb8e34d8fb80660 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 8 Mar 2026 23:11:22 +0100 Subject: [PATCH 05/51] fix comment order and not being able to move around comment nodes --- Source/Editor/Surface/ResizableSurfaceNode.cs | 38 ++++++++++++------- Source/Editor/Surface/SurfaceComment.cs | 30 +++++++-------- Source/Editor/Surface/SurfaceRootControl.cs | 6 +-- .../Editor/Surface/VisjectSurfaceContext.cs | 8 ++-- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 6fa7225c5..ea9ceec82 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -16,7 +16,7 @@ namespace FlaxEditor.Surface /// /// Helper class for that handles mouse interactions resizing the node itself. /// - protected class ResizeBorder : Control + public class ResizeBorder : ContainerControl { /// /// Distance to each of the 4 node edges that the cursor has to be so the user can resize along the direction of the edge. @@ -24,10 +24,14 @@ namespace FlaxEditor.Surface private const float BorderWidth = 15f; private readonly VisjectSurface Surface; - private readonly ResizableSurfaceNode ResizableNode; private Float2 _surfaceMouseLocation; private Float2 startResizingSize; + /// + /// The resizable node that this controls. + /// + public readonly ResizableSurfaceNode ResizableNode; + /// /// True if the mouse is at the border of the resizable node and not further away from the border than . /// @@ -63,7 +67,6 @@ namespace FlaxEditor.Surface } } - /// public ResizeBorder(VisjectSurface surface, ResizableSurfaceNode resizableNode) { @@ -100,7 +103,7 @@ namespace FlaxEditor.Surface ResizeDirection = new Float2(Mathf.Abs(rawResizeDirection.X) >= nodeHalfSizeNoBorder.X ? Mathf.Sign(rawResizeDirection.X) : 0, Mathf.Abs(rawResizeDirection.Y) >= nodeHalfSizeNoBorder.Y ? Mathf.Sign(rawResizeDirection.Y) : 0); - IsMouseOverResizeBorder = false;// onBorder && !inNode; + IsMouseOverResizeBorder = onBorder && !inNode; } private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end) @@ -186,6 +189,13 @@ namespace FlaxEditor.Surface base.OnMouseMove(location); } + /// + public override void OnMouseEnter(Float2 location) + { + Cursor = CursorType.Default; + base.OnMouseEnter(location); + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { @@ -243,7 +253,7 @@ namespace FlaxEditor.Surface /// /// Represents the border control used for resizing the associated element. /// - protected ResizeBorder ResizeBorderControl; + public ResizeBorder ResizeBorderControl; /// /// Index of the Float2 value in the node values list to store node size. @@ -270,6 +280,7 @@ namespace FlaxEditor.Surface Parent = Surface.SurfaceRoot, }; + Parent = ResizeBorderControl; ResizeBorderControl.MatchResizableNode(Size, Location); } @@ -280,13 +291,7 @@ namespace FlaxEditor.Surface base.OnLocationChanged(); } - ///// - //public override bool CanSelect(ref Float2 location) - //{ - // return base.CanSelect(ref location); - //} - - /// + /// public override void OnSurfaceLoaded(SurfaceNodeActions action) { // Reapply the curve node size @@ -314,9 +319,14 @@ namespace FlaxEditor.Surface base.Draw(); if (Surface.CanEdit && (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder)) - { Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); - } + } + + /// + public override void OnDestroy() + { + ResizeBorderControl.Parent = null; + base.OnDestroy(); } } } diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index c257dddd1..73f280d48 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -81,16 +81,12 @@ namespace FlaxEditor.Surface if (Values.Length < 4) { if (IndexInParent > 0) - { IndexInParent = 0; - ResizeBorderControl.IndexInParent = - 1; - } OrderValue = IndexInParent; } else if (OrderValue != -1) { - IndexInParent = OrderValue; - ResizeBorderControl.IndexInParent = OrderValue - 1; + ResizeBorderControl.IndexInParent = OrderValue; } } @@ -103,8 +99,8 @@ namespace FlaxEditor.Surface Color = ColorValue = Color.FromHSV(new Random().NextFloat(0, 360), 0.7f, 0.25f, 0.8f); if (OrderValue == -1) - OrderValue = Context.CommentCount - 1; - IndexInParent = OrderValue; + OrderValue = Context.CommentCount; + ResizeBorderControl.IndexInParent = OrderValue; } /// @@ -333,25 +329,25 @@ namespace FlaxEditor.Surface { cmOrder.ContextMenu.AddButton("Bring Forward", () => { - if (IndexInParent < Context.CommentCount - 1) - IndexInParent++; - OrderValue = IndexInParent; + if (ResizeBorderControl.IndexInParent < Context.CommentCount - 1) + ResizeBorderControl.IndexInParent++; + OrderValue = ResizeBorderControl.IndexInParent; }); cmOrder.ContextMenu.AddButton("Bring to Front", () => { - IndexInParent = Context.CommentCount - 1; - OrderValue = IndexInParent; + ResizeBorderControl.IndexInParent = Context.CommentCount - 1; + OrderValue = ResizeBorderControl.IndexInParent; }); cmOrder.ContextMenu.AddButton("Send Backward", () => { - if (IndexInParent > 0) - IndexInParent--; - OrderValue = IndexInParent; + if (ResizeBorderControl.IndexInParent > 0) + ResizeBorderControl.IndexInParent--; + OrderValue = ResizeBorderControl.IndexInParent; }); cmOrder.ContextMenu.AddButton("Send to Back", () => { - IndexInParent = 0; - OrderValue = IndexInParent; + ResizeBorderControl.IndexInParent = 0; + OrderValue = ResizeBorderControl.IndexInParent; }); } } diff --git a/Source/Editor/Surface/SurfaceRootControl.cs b/Source/Editor/Surface/SurfaceRootControl.cs index 15d4c43cc..c7639a27d 100644 --- a/Source/Editor/Surface/SurfaceRootControl.cs +++ b/Source/Editor/Surface/SurfaceRootControl.cs @@ -46,10 +46,10 @@ namespace FlaxEditor.Surface { var child = _children[i]; - if (child is SurfaceComment && child.Visible) + if (child is ResizableSurfaceNode.ResizeBorder border && border.ResizableNode is SurfaceComment comment2 && comment2.Visible) { - Render2D.PushTransform(ref child._cachedTransform); - child.Draw(); + Render2D.PushTransform(ref comment2._cachedTransform); + comment2.Draw(); Render2D.PopTransform(); } } diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 0d10aa230..be91e02ca 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -80,8 +80,9 @@ namespace FlaxEditor.Surface var result = new List(); for (int i = 0; i < RootControl.Children.Count; i++) { - if (RootControl.Children[i] is SurfaceComment comment) - result.Add(comment); + var child = RootControl.Children[i]; + if (child is ResizableSurfaceNode.ResizeBorder border && border.ResizableNode is SurfaceComment comment2) + result.Add(comment2); } return result; } @@ -101,7 +102,8 @@ namespace FlaxEditor.Surface int count = 0; for (int i = 0; i < RootControl.Children.Count; i++) { - if (RootControl.Children[i] is SurfaceComment) + var child = RootControl.Children[i]; + if (child is ResizableSurfaceNode.ResizeBorder border && border.ResizableNode is SurfaceComment) count++; } return count; From 20bfc209c59c2f5c86c228749c23262b7ee9a335 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 8 Mar 2026 23:29:20 +0100 Subject: [PATCH 06/51] fix auto move new comment to back --- Source/Editor/Surface/VisjectSurface.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index e3bb94bcc..69c44ab98 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -810,10 +810,11 @@ namespace FlaxEditor.Surface int lowestCommentOrder = int.MaxValue; for (int i = 0; i < selection.Count; i++) { - if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder) - continue; - hasCommentsSelected = true; - lowestCommentOrder = selection[i].IndexInParent; + if (selection[i] is ResizableSurfaceNode node && node is SurfaceComment && node.ResizeBorderControl.IndexInParent < lowestCommentOrder) + { + hasCommentsSelected = true; + lowestCommentOrder = node.ResizeBorderControl.IndexInParent; + } } return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1); From 681b303f2dc8b144308e449bbb1ef9e40b9461bc Mon Sep 17 00:00:00 2001 From: Saas Date: Mon, 9 Mar 2026 17:04:06 +0100 Subject: [PATCH 07/51] fix cursor shape when there are nested comments --- Source/Editor/Surface/ResizableSurfaceNode.cs | 65 +++++++++++-------- Source/Editor/Surface/SurfaceComment.cs | 2 + Source/Editor/Surface/VisjectSurface.cs | 1 + 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index ea9ceec82..26c64c382 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -23,10 +23,15 @@ namespace FlaxEditor.Surface /// private const float BorderWidth = 15f; - private readonly VisjectSurface Surface; + private readonly VisjectSurface _surface; private Float2 _surfaceMouseLocation; private Float2 startResizingSize; + /// + /// Wether to ignore the surface index in parent when updating the cursor type. Set to false for nodes that have order like . + /// + internal bool IgnoreSurfaceIndex = true; + /// /// The resizable node that this controls. /// @@ -67,10 +72,14 @@ namespace FlaxEditor.Surface } } - /// + /// + /// Creates a new instance of . + /// + /// The surface. + /// The this controls. public ResizeBorder(VisjectSurface surface, ResizableSurfaceNode resizableNode) { - Surface = surface; + _surface = surface; ResizableNode = resizableNode; } @@ -87,7 +96,7 @@ namespace FlaxEditor.Surface private void UpdateSurfaceMouseLocation() { - _surfaceMouseLocation = Surface.PointFromScreen(Input.MouseScreenPosition); + _surfaceMouseLocation = _surface.PointFromScreen(Input.MouseScreenPosition); } private void UpdateResizeFlags(Float2 mouseLocation) @@ -122,18 +131,10 @@ namespace FlaxEditor.Surface { var emptySize = ResizableNode.CalculateNodeSize(0, 0); ResizableNode.SizeValue = ResizableNode.Size - emptySize; - Surface.MarkAsEdited(false); + _surface.MarkAsEdited(false); } } - /// - public override void OnMouseLeave() - { - Cursor = CursorType.Default; - IsMouseOverResizeBorder = false; - base.OnMouseLeave(); - } - /// public override void OnMouseMove(Float2 location) { @@ -147,7 +148,7 @@ namespace FlaxEditor.Surface var resizeAxisPos = Float2.Clamp(ResizeDirection, Float2.Zero, Float2.One); var resizeAxisNeg = Float2.Clamp(-ResizeDirection, Float2.Zero, Float2.One); - var currentSurfaceMouse = Surface.PointFromScreen(Input.MouseScreenPosition); + var currentSurfaceMouse = _surface.PointFromScreen(Input.MouseScreenPosition); var delta = currentSurfaceMouse - _surfaceMouseLocation; // TODO: scale/size snapping? @@ -159,8 +160,6 @@ namespace FlaxEditor.Surface var emptySize = ResizableNode.CalculateNodeSize(0, 0); - // TODO: Fix: Can't move comments anymore - // TODO: If resize is blocked by min size and the user tries to increase the size again, wait until !blocked by min size to apply delta again // To do this, just record pos when starting to block by min size and if (cursorLocation > min) { ResizeAgain() } @@ -168,23 +167,26 @@ namespace FlaxEditor.Surface ResizableNode.Size = new Float2(Mathf.Max(ResizableNode.Size.X, ResizableNode.sizeMin.X), Mathf.Max(ResizableNode.Size.Y, ResizableNode.sizeMin.Y)); ResizableNode.Location += uiControlDelta * resizeAxisNeg; - // Only move if size wasn't clamped - - - //Debug.Log($"OLD: {oldSize} NEW: {ResizableNode.Size}"); - ResizableNode.SizeValue = ResizableNode.Size - emptySize; ResizableNode.SizeValue = new Float2(Mathf.Max(ResizableNode.SizeValue.X, ResizableNode.sizeMin.X), Mathf.Max(ResizableNode.SizeValue.Y, ResizableNode.sizeMin.Y)); + ResizableNode.CalculateNodeSize(ResizableNode.Size.X, ResizableNode.Size.Y); UpdateSurfaceMouseLocation(); MatchResizableNode(ResizableNode.Size, ResizableNode.Location); } - if (IsMouseOverResizeBorder) - Cursor = CursorType; - else - Cursor = CursorType.Default; + // Update the cursor shape + if (_surface.resizeableNodeIndexInParent <= IndexInParent || IgnoreSurfaceIndex) + { + if (!IgnoreSurfaceIndex) + _surface.resizeableNodeIndexInParent = IndexInParent; + + if (IsMouseOverResizeBorder) + Cursor = CursorType; + else + Cursor = CursorType.Default; + } base.OnMouseMove(location); } @@ -196,6 +198,15 @@ namespace FlaxEditor.Surface base.OnMouseEnter(location); } + /// + public override void OnMouseLeave() + { + Cursor = CursorType.Default; + IsMouseOverResizeBorder = false; + _surface.resizeableNodeIndexInParent = -1; // Will get updated by MouseMove again to match current index + base.OnMouseLeave(); + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { @@ -291,10 +302,10 @@ namespace FlaxEditor.Surface base.OnLocationChanged(); } - /// + /// public override void OnSurfaceLoaded(SurfaceNodeActions action) { - // Reapply the curve node size + // Reapply the node size var size = SizeValue; if (Surface != null && Surface.GridSnappingEnabled) size = Surface.SnapToGrid(size, true); diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 73f280d48..4966c83b3 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -65,6 +65,8 @@ namespace FlaxEditor.Surface EndEditOnClick = false, // We have to handle this ourselves, otherwise the textbox instantly loses focus when double-clicking the header HorizontalAlignment = TextAlignment.Center, }; + + ResizeBorderControl.IgnoreSurfaceIndex = false; } /// diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 69c44ab98..352bc1ebe 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -62,6 +62,7 @@ namespace FlaxEditor.Surface private int _selectedConnectionIndex; internal int _isUpdatingBoxTypes; + internal int resizeableNodeIndexInParent = -1; /// /// True if surface supports implicit casting of the FlaxEngine.Object types into Boolean value (as simple validate check). From 8488183558b97f4e6ae8a322f0c41a4cf99065d5 Mon Sep 17 00:00:00 2001 From: Saas Date: Mon, 9 Mar 2026 18:29:56 +0100 Subject: [PATCH 08/51] don't show resize hints when connecting --- Source/Editor/Surface/ResizableSurfaceNode.cs | 4 ++-- Source/Editor/Surface/SurfaceComment.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 26c64c382..08f0afe5d 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -177,7 +177,7 @@ namespace FlaxEditor.Surface } // Update the cursor shape - if (_surface.resizeableNodeIndexInParent <= IndexInParent || IgnoreSurfaceIndex) + if ((_surface.resizeableNodeIndexInParent <= IndexInParent || IgnoreSurfaceIndex) && !_surface.IsConnecting) { if (!IgnoreSurfaceIndex) _surface.resizeableNodeIndexInParent = IndexInParent; @@ -329,7 +329,7 @@ namespace FlaxEditor.Surface { base.Draw(); - if (Surface.CanEdit && (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder)) + if (Surface.CanEdit && !Surface.IsConnecting && (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder)) Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); } diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 4966c83b3..a8f3849d5 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -190,7 +190,7 @@ namespace FlaxEditor.Surface Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); // Resize - if (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder) + if ((ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder) && !Surface.IsConnecting) Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); } From 5ba8cddd65d6d812447b36fc182b71bec612c981 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 17 Mar 2026 19:57:03 +0100 Subject: [PATCH 09/51] improve resizing to behave more like expected --- Source/Editor/Surface/ResizableSurfaceNode.cs | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 08f0afe5d..2e684a59a 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -24,11 +24,12 @@ namespace FlaxEditor.Surface private const float BorderWidth = 15f; private readonly VisjectSurface _surface; - private Float2 _surfaceMouseLocation; + private Float2 _lastSurfaceMouseLoc; private Float2 startResizingSize; + private Float2 noClampedSize; /// - /// Wether to ignore the surface index in parent when updating the cursor type. Set to false for nodes that have order like . + /// Whether to ignore the surface index in parent when updating the cursor type. Set to false for nodes that have order like . /// internal bool IgnoreSurfaceIndex = true; @@ -94,16 +95,11 @@ namespace FlaxEditor.Surface Location = nodeLocation - new Float2(BorderWidth); } - private void UpdateSurfaceMouseLocation() - { - _surfaceMouseLocation = _surface.PointFromScreen(Input.MouseScreenPosition); - } - private void UpdateResizeFlags(Float2 mouseLocation) { var borderRect = Bounds with { Location = Float2.Zero }; bool onBorder = borderRect.Contains(mouseLocation); - // Check this the way we do because some resizeable nodes (like comments) have an implementation of `Control.ContainsPoint` + // Check this the way we do because some resizable nodes (like comments) have an implementation of `Control.ContainsPoint` // that does not check for the size you would think they have based on their visual appearance bool inNode = borderRect.MakeExpanded(-BorderWidth * 2f).Contains(mouseLocation); @@ -115,7 +111,7 @@ namespace FlaxEditor.Surface IsMouseOverResizeBorder = onBorder && !inNode; } - private Float2 GetControlDelta(Control control, ref Float2 start, ref Float2 end) + private Float2 GetControlDelta(Control control, Float2 start, Float2 end) { var pointOrigin = control.Parent ?? control; var startPos = pointOrigin.PointFromParent(ResizableNode, start); @@ -148,32 +144,30 @@ namespace FlaxEditor.Surface var resizeAxisPos = Float2.Clamp(ResizeDirection, Float2.Zero, Float2.One); var resizeAxisNeg = Float2.Clamp(-ResizeDirection, Float2.Zero, Float2.One); - var currentSurfaceMouse = _surface.PointFromScreen(Input.MouseScreenPosition); - var delta = currentSurfaceMouse - _surfaceMouseLocation; + var currentSurfaceMouseLoc = _surface.PointFromScreen(Input.MouseScreenPosition); + var delta = currentSurfaceMouseLoc - _lastSurfaceMouseLoc; - // TODO: scale/size snapping? + // TODO: Snapping delta *= resizeAxisAbs; + var moveLocation = currentSurfaceMouseLoc; + var uiControlDelta = GetControlDelta(this, _lastSurfaceMouseLoc, moveLocation); - var moveLocation = _surfaceMouseLocation + delta; - - var uiControlDelta = GetControlDelta(this, ref _surfaceMouseLocation, ref moveLocation); + // This ensures that the node is not resized below min size, but also keeps the resize behavior like expected (which just Max() -ing the value does not) var emptySize = ResizableNode.CalculateNodeSize(0, 0); + var minSize = ResizableNode.sizeMin; + noClampedSize = noClampedSize + uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; + if (noClampedSize.X < minSize.X && noClampedSize.X < ResizableNode.Size.X) + resizeAxisAbs.X = resizeAxisPos.X = resizeAxisNeg.X = 0f; + if (noClampedSize.Y < minSize.Y && noClampedSize.Y < ResizableNode.Size.Y) + resizeAxisAbs.Y = resizeAxisPos.Y = resizeAxisNeg.Y = 0f; - - // TODO: If resize is blocked by min size and the user tries to increase the size again, wait until !blocked by min size to apply delta again - // To do this, just record pos when starting to block by min size and if (cursorLocation > min) { ResizeAgain() } - - ResizableNode.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; - ResizableNode.Size = new Float2(Mathf.Max(ResizableNode.Size.X, ResizableNode.sizeMin.X), Mathf.Max(ResizableNode.Size.Y, ResizableNode.sizeMin.Y)); ResizableNode.Location += uiControlDelta * resizeAxisNeg; - ResizableNode.SizeValue = ResizableNode.Size - emptySize; - ResizableNode.SizeValue = new Float2(Mathf.Max(ResizableNode.SizeValue.X, ResizableNode.sizeMin.X), Mathf.Max(ResizableNode.SizeValue.Y, ResizableNode.sizeMin.Y)); ResizableNode.CalculateNodeSize(ResizableNode.Size.X, ResizableNode.Size.Y); - - UpdateSurfaceMouseLocation(); MatchResizableNode(ResizableNode.Size, ResizableNode.Location); + + _lastSurfaceMouseLoc = currentSurfaceMouseLoc; } // Update the cursor shape @@ -213,7 +207,8 @@ namespace FlaxEditor.Surface if (button == MouseButton.Left && IsMouseOverResizeBorder && !IsResizing) { // Start resizing - UpdateSurfaceMouseLocation(); + _lastSurfaceMouseLoc = _surface.PointFromScreen(Input.MouseScreenPosition); + noClampedSize = ResizableNode.Size; IsResizing = true; startResizingSize = ResizableNode.Size; StartMouseCapture(); From 1d18220971d873493813adb3c05009dfb49edd24 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 17 Mar 2026 20:17:05 +0100 Subject: [PATCH 10/51] remove debug draw --- Source/Editor/Surface/ResizableSurfaceNode.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 2e684a59a..8b06d2966 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -248,12 +248,6 @@ namespace FlaxEditor.Surface base.OnEndMouseCapture(); } - - /// - public override void Draw() - { - Render2D.DrawRectangle(Bounds with { Location = Float2.Zero }, Color.Green, 1f); - } } /// From 826b7841e6b95a59fc8f6f497d3bbc13d49b2567 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 17 Mar 2026 20:22:56 +0100 Subject: [PATCH 11/51] actually resize the node lol --- Source/Editor/Surface/ResizableSurfaceNode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 8b06d2966..edb6eac18 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -161,6 +161,7 @@ namespace FlaxEditor.Surface if (noClampedSize.Y < minSize.Y && noClampedSize.Y < ResizableNode.Size.Y) resizeAxisAbs.Y = resizeAxisPos.Y = resizeAxisNeg.Y = 0f; + ResizableNode.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; ResizableNode.Location += uiControlDelta * resizeAxisNeg; ResizableNode.SizeValue = ResizableNode.Size - emptySize; From 7bed68948b40845afa9bbeab4349cbafd68a9e18 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 17 Mar 2026 20:28:55 +0100 Subject: [PATCH 12/51] no resizing in non editable Visject surfaces --- Source/Editor/Surface/ResizableSurfaceNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index edb6eac18..9d44b325b 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -138,7 +138,7 @@ namespace FlaxEditor.Surface { UpdateResizeFlags(location); } - else + else if (_surface.CanEdit) { var resizeAxisAbs = ResizeDirection.Absolute; var resizeAxisPos = Float2.Clamp(ResizeDirection, Float2.Zero, Float2.One); @@ -172,7 +172,7 @@ namespace FlaxEditor.Surface } // Update the cursor shape - if ((_surface.resizeableNodeIndexInParent <= IndexInParent || IgnoreSurfaceIndex) && !_surface.IsConnecting) + if ((_surface.resizeableNodeIndexInParent <= IndexInParent || IgnoreSurfaceIndex) && !_surface.IsConnecting && _surface.CanEdit) { if (!IgnoreSurfaceIndex) _surface.resizeableNodeIndexInParent = IndexInParent; From c08661d150341240bdfe769c92c95f2a36c72385 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Perrier Date: Fri, 1 May 2026 22:56:22 +0200 Subject: [PATCH 13/51] Add AGENTS file --- AGENTS.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..8cb870037 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,126 @@ +# AGENTS.md + +## Repo Purpose + +Flax Engine is a modern 3D game engine written in C++ and C#. +This repository contains the engine, editor, tooling, shaders, tests, assets, and platform-specific sources, excluding NDA-protected platform support. + +## High-Level Structure + +- `Source/Engine/`: engine runtime code and managed/runtime integration. +- `Source/Editor/`: editor code in both C++ and C#. +- `Source/Tools/`: build system and developer tooling, including `Flax.Build`. +- `Source/Platforms/`: platform-specific code, dependencies, and binaries. +- `Source/ThirdParty/`: vendored third-party code. Avoid changes here unless the task explicitly requires it. +- `Source/Engine/Tests/`: native and managed engine tests. +- `Source/Tools/Flax.Build.Tests/`: .NET tests for the build tool. +- `Content/`: engine/editor assets. +- `Development/Scripts/`: helper scripts for project generation and builds. + +## Working Assumptions + +- Generated solutions and project files are not committed. On a clean clone, generate them first. +- Git LFS is required. The Windows build scripts explicitly check for LFS-populated files. +- On Windows, the main entry points are `GenerateProjectFiles.bat` and `Development\Scripts\Windows\CallBuildTool.bat`. +- Prefer edits in `Source/Engine`, `Source/Editor`, `Source/Tools`, or docs. Do not modify `Binaries/`, `Cache/`, or generated project files unless the task explicitly targets generated output. + +## Windows Setup And Build + +Use these commands from the repo root. + +Generate project files: + +```powershell +.\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs -dotnet=8 +``` + +Alternative default generation: + +```powershell +.\GenerateProjectFiles.bat +``` + +Build the editor target: + +```powershell +.\Development\Scripts\Windows\CallBuildTool.bat -build -log -dotnet=8 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor +``` + +Run the editor: + +```powershell +.\Binaries\Editor\Win64\Development\FlaxEditor.exe +``` + +Visual Studio workflow after generation: + +- Open `Flax.sln`. +- Use solution configuration `Editor.Development` and platform `Win64`. +- Set `Flax` or `FlaxEngine` as the startup project. + +## Tests And Validation + +The checked-in CI workflow in `.github/workflows/tests.yml` is the most reliable source for current validation commands. + +Build native tests: + +```powershell +.\Development\Scripts\Windows\CallBuildTool.bat -build -log -dotnet=8 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget +``` + +Run native tests: + +```powershell +.\Binaries\Editor\Win64\Development\FlaxTests.exe -headless +``` + +Build Flax.Build tests: + +```powershell +dotnet msbuild Source\Tools\Flax.Build.Tests\Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo +``` + +Run Flax.Build tests: + +```powershell +dotnet test -f net8.0 Binaries\Tests\Flax.Build.Tests.dll +``` + +Run managed engine tests after copying runtime dependencies: + +```powershell +xcopy /y Binaries\Editor\Win64\Development\FlaxEngine.CSharp.dll Binaries\Tests +xcopy /y Binaries\Editor\Win64\Development\FlaxEngine.CSharp.runtimeconfig.json Binaries\Tests +xcopy /y Binaries\Editor\Win64\Development\Newtonsoft.Json.dll Binaries\Tests +dotnet test -f net8.0 Binaries\Tests\FlaxEngine.CSharp.dll +``` + +If a change is localized, prefer the narrowest possible target build and only run the relevant tests for that area. + +## Style And Conventions + +- The root `Source/.editorconfig` sets CRLF line endings, UTF-8, final newline, and spaces for indentation. +- Use 4 spaces for C++, C#, shaders, and Python. Use 2 spaces for XAML and MSBuild files. +- Keep the existing copyright header in source files. +- Public APIs in both C++ headers and C# commonly use XML-style documentation comments such as `/// `. +- Preserve local file style instead of mass-normalizing. The C# codebase mixes block namespaces and file-scoped namespaces. +- Follow existing naming patterns: PascalCase for types and public members; private C# fields often use `_camelCase`. +- In C++ headers, prefer forward declarations where practical and keep includes minimal. + +## Build System Notes + +- `FlaxEditor` is the main standalone editor target. +- `FlaxGame` is the standalone game target. +- `FlaxTestsTarget` builds the native test executable. +- `Flax.Build` supports project generation switches such as `-vs2022`, `-vs2026`, `-vscode`, and `-rider`. +- `GenerateProjectFiles.bat` also builds C# bindings for `FlaxEditor` on Windows. + +## Agent Guidance + +- Treat `.github/workflows/tests.yml` as the source of truth for CI-backed validation. +- Do not assume generated artifacts already exist in the repo. +- Avoid broad style-only rewrites. +- Avoid touching `Source/ThirdParty/` unless explicitly requested. +- If a change affects developer workflow or build/test steps, update the relevant root docs as part of the task. +- No dedicated repo-wide formatter or linter command is checked in. Prefer build and test validation over inventing formatting steps. +- PVS-Studio is mentioned in `README.md`, but it is not wired here as a standard local validation command. From 272364c1a12e6bf463e3cf05dcab4b51b7085781 Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sat, 23 May 2026 12:41:41 +0300 Subject: [PATCH 14/51] Upd: Added support for client-side window decoratoration, fixed UI bugs --- .../Editor/GUI/ContextMenu/ContextMenuBase.cs | 4 +- Source/Editor/Options/InterfaceOptions.cs | 8 +- Source/Editor/Utilities/Utils.cs | 6 + Source/Engine/Platform/Mac/MacWindow.cpp | 117 +++++++++++++++--- Source/Engine/Platform/Mac/MacWindow.h | 6 +- 5 files changed, 122 insertions(+), 19 deletions(-) diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 3eace0363..3947430f9 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -1,8 +1,8 @@ -#if PLATFORM_WINDOWS || PLATFORM_SDL +#if PLATFORM_WINDOWS || PLATFORM_SDL || PLATFORM_MAC #define USE_IS_FOREGROUND #else #endif -#if PLATFORM_SDL +#if PLATFORM_SDL || PLATFORM_MAC #define USE_SDL_WORKAROUNDS #endif // Copyright (c) Wojciech Figat. All rights reserved. diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index 6d56e9ac7..47a4a7880 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -189,12 +189,18 @@ namespace FlaxEditor.Options /// /// Determined automatically based on the system and any known compatibility issues with native decorations. /// +#if PLATFORM_MAC && !PLATFORM_SDL + [HideInEditor] +#endif Auto, /// /// Automatically choose most compatible window decorations for child windows, prefer custom decorations on main window. /// [EditorDisplay(Name = "Auto (Child Only)")] +#if PLATFORM_MAC && !PLATFORM_SDL + [HideInEditor] +#endif AutoChildOnly, /// @@ -307,7 +313,7 @@ namespace FlaxEditor.Options [EditorDisplay("Interface"), EditorOrder(322)] public bool ScrollToScriptOnAdd { get; set; } = true; -#if PLATFORM_SDL +#if PLATFORM_SDL || PLATFORM_MAC /// /// Gets or sets a value indicating whether use native window title bar decorations in child windows. Editor restart required. /// diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 62af28bc0..5955ccf60 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -1293,6 +1293,12 @@ namespace FlaxEditor.Utilities }; #elif PLATFORM_WINDOWS return !Editor.Instance.Options.Options.Interface.UseNativeWindowSystem; +#elif PLATFORM_MAC + return Editor.Instance.Options.Options.Interface.WindowDecorations switch + { + Options.InterfaceOptions.WindowDecorationsType.ClientSide => true, + _ => false + }; #else return false; #endif diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index b7014681a..4c9e20d1a 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -200,6 +200,19 @@ Float2 GetMousePosition(MacWindow* window, NSEvent* event) return Float2(point.x, frame.size.height - point.y) * MacPlatform::ScreenScale - GetWindowTitleSize(window); } +NSRect GetFrameRectForClientBounds(MacWindow* macWindow, NSWindow* window, const Rectangle& clientArea) +{ + const float screenScale = MacPlatform::ScreenScale; + NSRect rect = NSMakeRect(0, 0, clientArea.Size.X / screenScale, clientArea.Size.Y / screenScale); + rect = [window frameRectForContentRect:rect]; + + Float2 pos = AppleUtils::PosToCoca(clientArea.Location) / screenScale; + Float2 titleSize = GetWindowTitleSize(macWindow); + rect.origin.x = pos.X + titleSize.X; + rect.origin.y = pos.Y - rect.size.height + titleSize.Y; + return rect; +} + class MacDropData : public IGuiData { public: @@ -310,6 +323,12 @@ NSDragOperation GetDragDropOperation(DragDropEffect dragDropEffect) Window->OnLostFocus(); } +- (void)windowDidMove:(NSNotification*)notification +{ + if (IsWindowInvalid(Window)) return; + Window->SyncWindowState(); +} + - (void)windowWillClose:(NSNotification*)notification { [self setDelegate: nil]; @@ -518,6 +537,28 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); mousePos = Window->ClientToScreen(mousePos); + + if ([event clickCount] == 1 && !Input::Mouse->IsRelative()) + { + WindowHitCodes hit = WindowHitCodes::Client; + bool handled = false; + Window->OnHitTest(mousePos, hit, handled); + + if (hit == WindowHitCodes::Caption) + { + bool consumed = false; + Window->OnLeftButtonHit(hit, consumed); + + if (!consumed) + { + [(NSWindow*)Window->GetNativePtr() performWindowDragWithEvent:event]; + Window->SyncWindowState(); + } + + return; + } + } + MouseButton mouseButton = MouseButton::Left; if ([event clickCount] == 2 && !Input::Mouse->IsRelative()) Input::Mouse->OnMouseDoubleClick(mousePos, mouseButton, Window); @@ -835,15 +876,28 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) MacWindow::~MacWindow() { - NSWindow* window = (NSWindow*)_window; - [window close]; - [window release]; + if (NSWindow* window = (NSWindow*)_window) + { + [window close]; + [window release]; + } _window = nullptr; _view = nullptr; } +void MacWindow::SyncWindowState() +{ + NSWindow* window = (NSWindow*)_window; + if (window) + { + _minimized = window.miniaturized; + _maximized = window.zoomed; + } +} + void MacWindow::CheckForResize(float width, float height) { + SyncWindowState(); const Float2 clientSize(width, height); if (clientSize != _clientSize) { @@ -940,7 +994,7 @@ void MacWindow::Hide() [window orderOut:nil]; // Transfer focus back to the parent when hiding popup - if (_settings.Parent && wasKey) + if (_settings.Parent && wasKey && _settings.Type != WindowType::Popup && _settings.Type != WindowType::Tooltip) { NSWindow* parent = (NSWindow*)_settings.Parent->GetNativePtr(); [parent makeKeyAndOrderFront:nil]; @@ -951,6 +1005,23 @@ void MacWindow::Hide() } } +void MacWindow::Close(ClosingReason reason) +{ + const BOOL wasKey = _window && [(NSWindow*)_window isKeyWindow]; + WindowBase::Close(reason); + + if (NSWindow* window = (NSWindow*)_window) + { + [window close]; + } + + if (_settings.Parent && wasKey && _settings.Type != WindowType::Popup && _settings.Type != WindowType::Tooltip) + { + NSWindow* parent = (NSWindow*)_settings.Parent->GetNativePtr(); + [parent makeKeyAndOrderFront:nil]; + } +} + void MacWindow::Minimize() { if (!_settings.AllowMinimize) @@ -967,17 +1038,43 @@ void MacWindow::Maximize() if (!_settings.AllowMaximize) return; NSWindow* window = (NSWindow*)_window; + if (!window) + return; if (!window.zoomed) + { + if (!_maximized) + { + _restoreClientBounds = GetClientBounds(); + _hasRestoreClientBounds = true; + } [window zoom:nil]; + } + SyncWindowState(); } void MacWindow::Restore() { NSWindow* window = (NSWindow*)_window; + if (!window) + return; if (window.miniaturized) + { [window deminiaturize:nil]; + SyncWindowState(); + } + else if (_maximized && _hasRestoreClientBounds) + { + const Rectangle restoreClientBounds = _restoreClientBounds; + _hasRestoreClientBounds = false; + NSRect restoreFrame = GetFrameRectForClientBounds(this, window, restoreClientBounds); + [window setFrame:restoreFrame display:YES animate:YES]; + _maximized = false; + } else if (window.zoomed) + { [window zoom:nil]; + SyncWindowState(); + } } bool MacWindow::IsForegroundWindow() const @@ -1001,17 +1098,7 @@ void MacWindow::SetClientBounds(const Rectangle& clientArea) NSWindow* window = (NSWindow*)_window; if (!window) return; - const float screenScale = MacPlatform::ScreenScale; - - NSRect oldRect = [window frame]; - NSRect newRect = NSMakeRect(0, 0, clientArea.Size.X / screenScale, clientArea.Size.Y / screenScale); - newRect = [window frameRectForContentRect:newRect]; - - Float2 pos = AppleUtils::PosToCoca(clientArea.Location) / screenScale; - Float2 titleSize = GetWindowTitleSize(this); - newRect.origin.x = pos.X + titleSize.X; - newRect.origin.y = pos.Y - newRect.size.height + titleSize.Y; - + NSRect newRect = GetFrameRectForClientBounds(this, window, clientArea); [window setFrame:newRect display:YES]; } diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h index f4a8cb706..91a1219e0 100644 --- a/Source/Engine/Platform/Mac/MacWindow.h +++ b/Source/Engine/Platform/Mac/MacWindow.h @@ -17,13 +17,16 @@ private: void* _window = nullptr; void* _view = nullptr; bool _isMouseOver = false; + bool _hasRestoreClientBounds = false; + Rectangle _restoreClientBounds; Float2 _mouseTrackPos = Float2::Minimum; String _dragText; public: MacWindow(const CreateWindowSettings& settings); - ~MacWindow(); + ~MacWindow() override; + void SyncWindowState(); void CheckForResize(float width, float height); void SetIsMouseOver(bool value); const String& GetDragText() const @@ -37,6 +40,7 @@ public: void OnUpdate(float dt) override; void Show() override; void Hide() override; + void Close(ClosingReason reason) override; void Minimize() override; void Maximize() override; void Restore() override; From 3f5c21b4b669de4a38156ddb85d21ceed9258ed6 Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sun, 24 May 2026 22:57:59 +0300 Subject: [PATCH 15/51] Upd: Fixed double closing. --- Source/Engine/Platform/Mac/MacWindow.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 4c9e20d1a..adb33962f 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -1009,6 +1009,10 @@ void MacWindow::Close(ClosingReason reason) { const BOOL wasKey = _window && [(NSWindow*)_window isKeyWindow]; WindowBase::Close(reason); + + // Closing can be cancelled by managed Window.Closing handlers. + if (!IsClosed()) + return; if (NSWindow* window = (NSWindow*)_window) { From 7326cf65d8445daee79f5642c9c6d40965549b85 Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Wed, 3 Jun 2026 18:49:42 +0300 Subject: [PATCH 16/51] Fix: Editor - use non-jittered projection for post-TAA overlay grid Add a managed `RenderView.GetOverlayProjection()` helper that returns the non-jittered projection once TAA has been resolved, and use it for editor overlay rendering paths. --- Source/Editor/Gizmo/GridGizmo.cs | 3 ++- Source/Engine/Graphics/RenderView.cs | 8 ++++++++ Source/Engine/UI/UICanvas.cs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Gizmo/GridGizmo.cs b/Source/Editor/Gizmo/GridGizmo.cs index 71f20f8c2..37cbd20ab 100644 --- a/Source/Editor/Gizmo/GridGizmo.cs +++ b/Source/Editor/Gizmo/GridGizmo.cs @@ -113,7 +113,8 @@ namespace FlaxEditor.Gizmo if (cb != IntPtr.Zero) { var data = new Data(); - Matrix.Multiply(ref renderContext.View.View, ref renderContext.View.Projection, out var viewProjection); + var projection = renderContext.View.GetOverlayProjection(); + Matrix.Multiply(ref renderContext.View.View, ref projection, out var viewProjection); Matrix.Transpose(ref viewProjection, out data.ViewProjectionMatrix); data.ViewPos = renderContext.View.WorldPosition; data.GridColor = options.Viewport.ViewportGridColor; diff --git a/Source/Engine/Graphics/RenderView.cs b/Source/Engine/Graphics/RenderView.cs index b71b2d19b..66e3546ff 100644 --- a/Source/Engine/Graphics/RenderView.cs +++ b/Source/Engine/Graphics/RenderView.cs @@ -33,6 +33,14 @@ namespace FlaxEngine NonJitteredProjection = Projection; } + /// + /// Gets projection matrix for overlay geometry rendered after temporal anti-aliasing has been resolved. + /// + public Matrix GetOverlayProjection() + { + return IsTaaResolved ? NonJitteredProjection : Projection; + } + /// /// Initializes render view data. /// diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index 225213dba..a4cf2fd78 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -90,7 +90,7 @@ namespace FlaxEngine Matrix.Multiply(ref worldMatrix, ref renderContext.View.View, out Matrix viewMatrix); Matrix projectionMatrix = renderContext.View.Projection; if (worldSpace && (Canvas.RenderLocation == PostProcessEffectLocation.Default || Canvas.RenderLocation == PostProcessEffectLocation.AfterAntiAliasingPass)) - projectionMatrix = renderContext.View.NonJitteredProjection; // Fix TAA jittering when rendering UI in world after TAA resolve + projectionMatrix = renderContext.View.GetOverlayProjection(); // Fix TAA jittering when rendering UI in world after TAA resolve Matrix.Multiply(ref viewMatrix, ref projectionMatrix, out Matrix viewProjectionMatrix); // Pick a depth buffer From fc9e24e6fed088fc1df4668be6e7aae10fb2878a Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 3 Jun 2026 21:43:02 +0200 Subject: [PATCH 17/51] include hint that fov needs to be in radians in doc comments --- Source/Engine/Core/Math/BoundingFrustum.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Core/Math/BoundingFrustum.cs b/Source/Engine/Core/Math/BoundingFrustum.cs index 4f1e27e1e..e0d4e0d2b 100644 --- a/Source/Engine/Core/Math/BoundingFrustum.cs +++ b/Source/Engine/Core/Math/BoundingFrustum.cs @@ -264,7 +264,7 @@ namespace FlaxEngine /// The camera pos. /// The look dir. /// Up dir. - /// The fov. + /// The fov in radians. /// The Z near. /// The Z far. /// The aspect. From 78254afd986b92e95c89a2f1c7d8b47474a16689 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 3 Jun 2026 22:00:23 +0200 Subject: [PATCH 18/51] more doc comment improvements for BoundingFrustrum cs --- Source/Engine/Core/Math/BoundingFrustum.cs | 89 +++++++++++----------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/Source/Engine/Core/Math/BoundingFrustum.cs b/Source/Engine/Core/Math/BoundingFrustum.cs index e0d4e0d2b..e2b1e781c 100644 --- a/Source/Engine/Core/Math/BoundingFrustum.cs +++ b/Source/Engine/Core/Math/BoundingFrustum.cs @@ -259,29 +259,29 @@ namespace FlaxEngine } /// - /// Creates a new frustum relaying on perspective camera parameters + /// Creates a new frustum based on a perspective camera parameters. /// - /// The camera pos. - /// The look dir. - /// Up dir. + /// The camera position. + /// The look direction. + /// Up direction. /// The fov in radians. - /// The Z near. - /// The Z far. - /// The aspect. - /// The bounding frustum calculated from perspective camera - public static BoundingFrustum FromCamera(Vector3 cameraPos, Vector3 lookDir, Vector3 upDir, float fov, float znear, float zfar, float aspect) + /// The Z near plane. + /// The Z far plane. + /// The aspect ratio. + /// The bounding frustum calculated from the perspective camera + public static BoundingFrustum FromCamera(Vector3 cameraPos, Vector3 lookDir, Vector3 upDir, float fov, float zNear, float zFar, float aspectRatio) { //http://knol.google.com/k/view-frustum lookDir = Vector3.Normalize(lookDir); upDir = Vector3.Normalize(upDir); - Vector3 nearCenter = cameraPos + lookDir * znear; - Vector3 farCenter = cameraPos + lookDir * zfar; - var nearHalfHeight = (float)(znear * Math.Tan(fov / 2f)); - var farHalfHeight = (float)(zfar * Math.Tan(fov / 2f)); - float nearHalfWidth = nearHalfHeight * aspect; - float farHalfWidth = farHalfHeight * aspect; + Vector3 nearCenter = cameraPos + lookDir * zNear; + Vector3 farCenter = cameraPos + lookDir * zFar; + var nearHalfHeight = (float)(zNear * Math.Tan(fov / 2f)); + var farHalfHeight = (float)(zFar * Math.Tan(fov / 2f)); + float nearHalfWidth = nearHalfHeight * aspectRatio; + float farHalfWidth = farHalfHeight * aspectRatio; Vector3 rightDir = Vector3.Normalize(Vector3.Cross(upDir, lookDir)); Vector3 near1 = nearCenter - nearHalfHeight * upDir + nearHalfWidth * rightDir; @@ -310,20 +310,21 @@ namespace FlaxEngine result.pTop.Normalize(); result.pBottom.Normalize(); - result.pMatrix = Matrix.LookAt(cameraPos, cameraPos + lookDir * 10, upDir) * Matrix.PerspectiveFov(fov, aspect, znear, zfar); + result.pMatrix = Matrix.LookAt(cameraPos, cameraPos + lookDir * 10, upDir) * Matrix.PerspectiveFov(fov, aspectRatio, zNear, zFar); return result; } /// - /// Returns the 8 corners of the frustum, element0 is Near1 (near right down corner) - /// , element1 is Near2 (near right top corner) - /// , element2 is Near3 (near Left top corner) - /// , element3 is Near4 (near Left down corner) - /// , element4 is Far1 (far right down corner) - /// , element5 is Far2 (far right top corner) - /// , element6 is Far3 (far left top corner) - /// , element7 is Far4 (far left down corner) + /// Returns the 8 corners of the frustum: + /// [0] is Near1 (Near right down corner) + /// [1] is Near2 (Near right top corner) + /// [2] is Near3 (Near left top corner) + /// [3] is Near4 (Near left down corner) + /// [4] is Far1 (Far right down corner) + /// [5] is Far2 (Far right top corner) + /// [6] is Far3 (Far left top corner) + /// [7] is Far4 (Far left down corner) /// /// The 8 corners of the frustum public Vector3[] GetCorners() @@ -334,16 +335,16 @@ namespace FlaxEngine } /// - /// Returns the 8 corners of the frustum, element0 is Near1 (near right down corner) - /// , element1 is Near2 (near right top corner) - /// , element2 is Near3 (near Left top corner) - /// , element3 is Near4 (near Left down corner) - /// , element4 is Far1 (far right down corner) - /// , element5 is Far2 (far right top corner) - /// , element6 is Far3 (far left top corner) - /// , element7 is Far4 (far left down corner) + /// Populates the array with the 8 corners of the frustum: + /// [0] is Near1 (Near right down corner) + /// [1] is Near2 (Near right top corner) + /// [2] is Near3 (Near left top corner) + /// [3] is Near4 (Near left down corner) + /// [4] is Far1 (Far right down corner) + /// [5] is Far2 (Far right top corner) + /// [6] is Far3 (Far left top corner) + /// [7] is Far4 (Far left down corner) /// - /// The 8 corners of the frustum public void GetCorners(Vector3[] corners) { corners[0] = Get3PlanesInterPoint(ref pNear, ref pBottom, ref pRight); //Near1 @@ -357,7 +358,7 @@ namespace FlaxEngine } /// - /// Checks whether a point lay inside, intersects or lay outside the frustum. + /// Checks whether a point lays inside, intersects or lays outside the frustum. /// /// The point. /// Type of the containment @@ -664,15 +665,15 @@ namespace FlaxEngine { if (Contains(ray.Position) != ContainmentType.Disjoint) { - Real nearstPlaneDistance = Real.MaxValue; + Real nearestPlaneDistance = Real.MaxValue; for (var i = 0; i < 6; i++) { Plane plane = GetPlane(i); - if (CollisionsHelper.RayIntersectsPlane(ref ray, ref plane, out Real distance) && (distance < nearstPlaneDistance)) - nearstPlaneDistance = distance; + if (CollisionsHelper.RayIntersectsPlane(ref ray, ref plane, out Real distance) && (distance < nearestPlaneDistance)) + nearestPlaneDistance = distance; } - inDistance = nearstPlaneDistance; + inDistance = nearestPlaneDistance; outDistance = null; return true; } @@ -706,9 +707,9 @@ namespace FlaxEngine } /// - /// Get the distance which when added to camera position along the lookat direction will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view. - /// if the returned value is positive, the camera will move toward the lookat direction (ZoomIn). - /// if the returned value is negative, the camera will move in the reverse direction of the lookat direction (ZoomOut). + /// Get the distance which when added to camera position along the look-at direction will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view. + /// if the returned value is positive, the camera will move toward the look-at direction (ZoomIn). + /// if the returned value is negative, the camera will move in the reverse direction of the look-at direction (ZoomOut). /// /// The points. /// The zoom to fit distance @@ -735,9 +736,9 @@ namespace FlaxEngine } /// - /// Get the distance which when added to camera position along the lookat direction will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view. - /// if the returned value is positive, the camera will move toward the lookat direction (ZoomIn). - /// if the returned value is negative, the camera will move in the reverse direction of the lookat direction (ZoomOut). + /// Get the distance which when added to camera position along the look-at direction will do the effect of zoom to extents (zoom to fit) operation, so all the passed points will fit in the current view. + /// if the returned value is positive, the camera will move toward the look-at direction (ZoomIn). + /// if the returned value is negative, the camera will move in the reverse direction of the look-at direction (ZoomOut). /// /// The bounding box. /// The zoom to fit distance From f41e32683ab21dd610c58e3689cb8c3278f4e9ca Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 7 Jun 2026 22:50:03 +0200 Subject: [PATCH 19/51] Fix building deps for Windows ARM64 --- Source/Engine/Platform/Platform.Build.cs | 2 +- .../Flax.Build/Deps/Dependencies/basis_universal.cs | 2 ++ Source/Tools/Flax.Build/Deps/Dependencies/dbghelp.cs | 3 +++ Source/Tools/Flax.Build/Deps/Dependencies/glslang.cs | 12 +++++++++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Platform/Platform.Build.cs b/Source/Engine/Platform/Platform.Build.cs index cf16aee2c..b67158f4a 100644 --- a/Source/Engine/Platform/Platform.Build.cs +++ b/Source/Engine/Platform/Platform.Build.cs @@ -36,7 +36,7 @@ public class Platform : EngineModule { options.OutputFiles.Add("dbghelp.lib"); options.DelayLoadLibraries.Add("dbghelp.dll"); - options.DependencyFiles.Add(Path.Combine(options.DepsFolder, "dbghelp.dll")); + options.OptionalDependencyFiles.Add(Path.Combine(options.DepsFolder, "dbghelp.dll")); } if (options.Target.IsEditor) { diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/basis_universal.cs b/Source/Tools/Flax.Build/Deps/Dependencies/basis_universal.cs index 4bb7635fe..1928fe5ef 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/basis_universal.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/basis_universal.cs @@ -134,6 +134,8 @@ namespace Flax.Deps.Dependencies case TargetPlatform.Windows: { cmakeArgs = ".. -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL " + cmakeArgs; + if (architecture == TargetArchitecture.ARM64) + cmakeArgs += " -DBASISU_SSE=OFF"; RunCmake(buildDir, platform, architecture, cmakeArgs); BuildCmake(buildDir, configuration, options:Utilities.RunOptions.ConsoleLogOutput); CopyLib(platform, Path.Combine(buildDir, configuration), depsFolder, "basisu_encoder"); diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/dbghelp.cs b/Source/Tools/Flax.Build/Deps/Dependencies/dbghelp.cs index 34fac56e0..c50ce383d 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/dbghelp.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/dbghelp.cs @@ -48,6 +48,9 @@ namespace Flax.Deps.Dependencies } } + /// + public override bool BuildByDefault => false; + /// public override void Build(BuildOptions options) { diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/glslang.cs b/Source/Tools/Flax.Build/Deps/Dependencies/glslang.cs index 32c14a037..ff1d6a3ed 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/glslang.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/glslang.cs @@ -82,9 +82,19 @@ namespace Flax.Deps.Dependencies CloneGitRepoFast(root, "https://github.com/FlaxEngine/glslang.git"); // Setup the external sources - // Requires distutils (pip install setuptools) + bool canRetry = true; + RETRY: if (Utilities.Run(BuildPlatform != TargetPlatform.Mac ? "python" : "python3", "update_glslang_sources.py", null, root, Utilities.RunOptions.ConsoleLogOutput) != 0) + { + if (canRetry) + { + // Requires distutils (pip install setuptools) + canRetry = false; + Utilities.Run("pip", "install setuptools", null, root, Utilities.RunOptions.ConsoleLogOutput); + goto RETRY; + } throw new Exception("Failed to update glslang sources, make sure setuptools python package is installed."); + } foreach (var platform in options.Platforms) { From be5e13301e35c9fb2a63f10acf7d712c71a76fa0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 7 Jun 2026 23:16:23 +0200 Subject: [PATCH 20/51] Fix displaying 'No Camera' overlay in editor viewport for game when using custom rendering --- Source/Editor/Windows/GameWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 7b7370c26..d25e89f36 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -821,7 +821,8 @@ namespace FlaxEditor.Windows { base.Draw(); - if (Camera.MainCamera == null) + var mainRenderTask = MainRenderTask.Instance; + if (Camera.MainCamera == null && (mainRenderTask == null || !mainRenderTask.IsCustomRendering)) { var style = Style.Current; Render2D.DrawText(style.FontLarge, "No camera", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center); From c2da5a363ddc6b61214cdb95390d508a3ff464fb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 8 Jun 2026 21:09:31 +0200 Subject: [PATCH 21/51] Fix missing default interface style on macOS #4116 --- Source/Editor/Options/InterfaceOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index 47a4a7880..a12b2fda0 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -317,14 +317,14 @@ namespace FlaxEditor.Options /// /// Gets or sets a value indicating whether use native window title bar decorations in child windows. Editor restart required. /// -#if PLATFORM_WINDOWS +#if PLATFORM_WINDOWS || PLATFORM_MAC [DefaultValue(WindowDecorationsType.ClientSide)] #else [DefaultValue(WindowDecorationsType.AutoChildOnly)] #endif [EditorDisplay("Tabs & Windows"), EditorOrder(70), Tooltip("Determines whether use native window title bar decorations. Editor restart required.")] public WindowDecorationsType WindowDecorations { get; set; } = -#if PLATFORM_WINDOWS +#if PLATFORM_WINDOWS || PLATFORM_MAC WindowDecorationsType.ClientSide; #else WindowDecorationsType.AutoChildOnly; From cc79a39f71dee287b1ee7029a29da7dfe7ab6983 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 8 Jun 2026 21:09:45 +0200 Subject: [PATCH 22/51] Fix Variant warning regression --- Source/Engine/Core/Types/Variant.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 9532d4ec4..13c0a5a79 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -10,6 +10,7 @@ struct Transform; template class AssetReference; struct ScriptingTypeHandle; +template ScriptingTypeHandle StaticType(); /// /// Represents an object type that can be interpreted as more than one type. From 050de578a97dda4c5e79f3b162099bdb71b5328b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 8 Jun 2026 22:04:09 +0200 Subject: [PATCH 23/51] Add MacWindow logo loading and use correct one for Editor --- Source/Editor/Cooker/GameCooker.cpp | 2 +- Source/Editor/Cooker/Steps/ValidateStep.cpp | 6 ---- Source/Editor/Editor.cpp | 2 +- Source/Engine/Platform/Mac/MacWindow.cpp | 34 +++++++++++++++++++ Source/Engine/Platform/Mac/MacWindow.h | 1 + Source/FlaxEditor.Build.cs | 3 +- Source/Platforms/Mac/Logo.png | 3 ++ .../Flax.Build/Deploy/Deployment.Editor.cs | 3 +- 8 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 Source/Platforms/Mac/Logo.png diff --git a/Source/Editor/Cooker/GameCooker.cpp b/Source/Editor/Cooker/GameCooker.cpp index 98cf609a7..969e71b71 100644 --- a/Source/Editor/Cooker/GameCooker.cpp +++ b/Source/Editor/Cooker/GameCooker.cpp @@ -239,7 +239,7 @@ String CookingData::GetGameBinariesPath() const archDir = TEXT("ARM64"); break; default: - CRASH; + CRASH; return String::Empty; } diff --git a/Source/Editor/Cooker/Steps/ValidateStep.cpp b/Source/Editor/Cooker/Steps/ValidateStep.cpp index cf7cab4dc..b18994f9b 100644 --- a/Source/Editor/Cooker/Steps/ValidateStep.cpp +++ b/Source/Editor/Cooker/Steps/ValidateStep.cpp @@ -69,8 +69,6 @@ bool ValidateStep::Perform(CookingData& data) return true; } - // TODO: validate version - AssetInfo info; if (!Content::GetAssetInfo(gameSettings->FirstScene, info)) { @@ -79,9 +77,5 @@ bool ValidateStep::Perform(CookingData& data) } } - // TODO: validate more game config - - // TODO: validate all input scenes? - return false; } diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index 3806317b3..18bf43a3a 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -654,7 +654,7 @@ Window* Editor::CreateMainWindow() PROFILE_MEM(Editor); Window* window = Managed->GetMainWindow(); -#if PLATFORM_LINUX || (PLATFORM_MAC && PLATFORM_SDL) +#if PLATFORM_LINUX || PLATFORM_MAC // Set window icon const String iconPath = Globals::BinariesFolder / TEXT("Logo.png"); if (FileSystem::FileExists(iconPath)) diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index adb33962f..55b25a314 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -15,6 +15,7 @@ #include "Engine/Input/Mouse.h" #include "Engine/Input/Keyboard.h" #include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/Textures/TextureData.h" #include #include #include @@ -1302,4 +1303,37 @@ void MacWindow::SetCursor(CursorType type) } } +void MacWindow::SetIcon(TextureData& icon) +{ + // Get pixels + Array colorData; + icon.GetPixels(colorData); + + // Convert to Cocoa image + NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(icon.Width, icon.Height)]; + if (image == nil) + return; + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL + pixelsWide:icon.Width + pixelsHigh:icon.Height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:icon.Width * 4 + bitsPerPixel:32]; + if (rep == nil) + return; + + // Copy the pixels + Platform::MemoryCopy([rep bitmapData], colorData.Get(), colorData.Count() * sizeof(Color32)); + + // Add the image representation + [image addRepresentation:rep]; + + // Set app icon + [NSApp setApplicationIconImage:image]; +} + #endif diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h index 91a1219e0..76a2690a8 100644 --- a/Source/Engine/Platform/Mac/MacWindow.h +++ b/Source/Engine/Platform/Mac/MacWindow.h @@ -63,6 +63,7 @@ public: void StartTrackingMouse(bool useMouseScreenOffset) override; void EndTrackingMouse() override; void SetCursor(CursorType type) override; + void SetIcon(TextureData& icon) override; }; #endif diff --git a/Source/FlaxEditor.Build.cs b/Source/FlaxEditor.Build.cs index 5019dbb66..7b422a8a6 100644 --- a/Source/FlaxEditor.Build.cs +++ b/Source/FlaxEditor.Build.cs @@ -71,8 +71,7 @@ public class FlaxEditor : EngineTarget break; case TargetPlatform.Mac: options.OutputFolder = Path.Combine(options.WorkingDirectory, "Binaries", "Editor", "Mac", options.Configuration.ToString()); - if (EngineConfiguration.WithSDL(options)) - options.DependencyFiles.Add(Path.Combine(Globals.EngineRoot, "Source", "Logo.png")); + options.DependencyFiles.Add(Path.Combine(Globals.EngineRoot, "Source", "Platforms", "Mac", "Logo.png")); break; default: throw new InvalidPlatformException(options.Platform.Target, "Not supported Editor platform."); } diff --git a/Source/Platforms/Mac/Logo.png b/Source/Platforms/Mac/Logo.png new file mode 100644 index 000000000..06bdecf46 --- /dev/null +++ b/Source/Platforms/Mac/Logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9de813ad971bee3cffc5c0dcaffeed61ea56f97526d021e79f267db025f09414 +size 80147 diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 8c1fb83d5..bbd6bf167 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -357,8 +357,7 @@ namespace Flax.Deploy DeployFile(src, dst, "MoltenVK_icd.json"); DeployFiles(src, dst, "*.dll"); DeployFiles(src, dst, "*.dylib"); - if (EngineConfiguration.UseSDL && MacConfiguration.UseSDL) - DeployFile(src, dst, "Logo.png"); + DeployFile(src, dst, "Logo.png"); // Optimize package size Utilities.Run("strip", "FlaxEditor", null, dst, Utilities.RunOptions.None); From 2d793f685fc41916c6fc42c63623395574221146 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 8 Jun 2026 22:39:14 +0200 Subject: [PATCH 24/51] Add Volume to audio clip import options --- .../Engine/ContentImporters/ImportAudio.cpp | 27 +++++++++++++------ Source/Engine/Platform/Mac/MacWindow.cpp | 1 + Source/Engine/Tools/AudioTool/AudioTool.cpp | 22 +-------------- Source/Engine/Tools/AudioTool/AudioTool.h | 11 +++++--- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/Source/Engine/ContentImporters/ImportAudio.cpp b/Source/Engine/ContentImporters/ImportAudio.cpp index 2c5b84d49..1ff526390 100644 --- a/Source/Engine/ContentImporters/ImportAudio.cpp +++ b/Source/Engine/ContentImporters/ImportAudio.cpp @@ -85,21 +85,31 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& LOG(Info, "Audio: {0}kHz, channels: {1}, Bit depth: {2}, Length: {3}s", info.SampleRate / 1000.0f, info.NumChannels, info.BitDepth, info.GetLength()); // Load the whole audio data - uint32 bytesPerSample = info.BitDepth / 8; - uint32 bufferSize = info.NumSamples * bytesPerSample; DataContainer sampleBuffer; - sampleBuffer.Link(audioData.Get()); + sampleBuffer.Link(audioData.Get(), info.NumSamples * (info.BitDepth / 8)); + + if (!Math::IsOne(options.Volume)) + { + // Scale PCM signal + Array pcm; + pcm.Resize(info.NumSamples); + AudioTool::ConvertToFloat(sampleBuffer.Get(), info.BitDepth, pcm.Get(), info.NumSamples); + for (float& e : pcm) + e *= options.Volume; + sampleBuffer.Allocate(info.NumSamples * sizeof(int32)); + AudioTool::ConvertFromFloat(pcm.Get(), (int32*)sampleBuffer.Get(), info.NumSamples); + info.BitDepth = 32; + } // Convert bit depth if need to uint32 outputBitDepth = (uint32)options.BitDepth; if (outputBitDepth != info.BitDepth) { + DataContainer sampleBufferPrev = MoveTemp(sampleBuffer); const uint32 outBufferSize = info.NumSamples * (outputBitDepth / 8); sampleBuffer.Allocate(outBufferSize); - AudioTool::ConvertBitDepth(audioData.Get(), info.BitDepth, sampleBuffer.Get(), outputBitDepth, info.NumSamples); + AudioTool::ConvertBitDepth(sampleBufferPrev.Get(), info.BitDepth, sampleBuffer.Get(), outputBitDepth, info.NumSamples); info.BitDepth = outputBitDepth; - bytesPerSample = info.BitDepth / 8; - bufferSize = outBufferSize; } // Base @@ -157,13 +167,14 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& if (context.AllocateChunk(0)) return CreateAssetResult::CannotAllocateChunk; - WRITE_DATA(0, sampleBuffer.Get(), bufferSize); + WRITE_DATA(0, sampleBuffer.Get(), sampleBuffer.Length()); } else { // Split audio data into a several chunks (uniform data spread) const uint32 minChunkSize = 1 * 1024 * 1024; // 1 MB - const uint32 dataAlignment = info.NumChannels * bytesPerSample * ASSET_FILE_DATA_CHUNKS; // Ensure to never split samples in-between (eg. 24-bit that uses 3 bytes) + const uint32 bufferSize = sampleBuffer.Length(); + const uint32 dataAlignment = info.NumChannels * (info.BitDepth / 8) * ASSET_FILE_DATA_CHUNKS; // Ensure to never split samples in-between (eg. 24-bit that uses 3 bytes) const uint32 chunkSize = Math::AlignUp(Math::Max(minChunkSize, bufferSize / ASSET_FILE_DATA_CHUNKS), dataAlignment); const int32 chunksCount = Math::CeilToInt((float)bufferSize / (float)chunkSize); ASSERT(chunksCount > 0 && chunksCount <= ASSET_FILE_DATA_CHUNKS); diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 55b25a314..b98fb46ec 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -11,6 +11,7 @@ #include "Engine/Platform/Base/DragDropHelper.h" #endif #include "Engine/Core/Log.h" +#include "Engine/Core/Math/Color32.h" #include "Engine/Input/Input.h" #include "Engine/Input/Mouse.h" #include "Engine/Input/Keyboard.h" diff --git a/Source/Engine/Tools/AudioTool/AudioTool.cpp b/Source/Engine/Tools/AudioTool/AudioTool.cpp index d456d7aed..1cbc4042c 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.cpp +++ b/Source/Engine/Tools/AudioTool/AudioTool.cpp @@ -21,27 +21,7 @@ String AudioTool::Options::ToString() const { - return String::Format(TEXT("Format:{}, DisableStreaming:{}, Is3D:{}, Quality:{}, BitDepth:{}"), ScriptingEnum::ToString(Format), DisableStreaming, Is3D, Quality, (int32)BitDepth); -} - -void AudioTool::Options::Serialize(SerializeStream& stream, const void* otherObj) -{ - SERIALIZE_GET_OTHER_OBJ(AudioTool::Options); - - SERIALIZE(Format); - SERIALIZE(DisableStreaming); - SERIALIZE(Is3D); - SERIALIZE(Quality); - SERIALIZE(BitDepth); -} - -void AudioTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - DESERIALIZE(Format); - DESERIALIZE(DisableStreaming); - DESERIALIZE(Is3D); - DESERIALIZE(Quality); - DESERIALIZE(BitDepth); + return String::Format(TEXT("Volume: {}, Format:{}, DisableStreaming:{}, Is3D:{}, Quality:{}, BitDepth:{}"), Volume, ScriptingEnum::ToString(Format), DisableStreaming, Is3D, Quality, (int32)BitDepth); } #endif diff --git a/Source/Engine/Tools/AudioTool/AudioTool.h b/Source/Engine/Tools/AudioTool/AudioTool.h index 289b61c72..9ac1822de 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.h +++ b/Source/Engine/Tools/AudioTool/AudioTool.h @@ -42,6 +42,13 @@ public: API_STRUCT(Attributes="HideInEditor") struct FLAXENGINE_API Options : public ISerializable { DECLARE_SCRIPTING_TYPE_MINIMAL(Options); + API_AUTO_SERIALIZATION(); + + /// + /// The audio volume. Can be used to scale source audio data at import time. + /// + API_FIELD(Attributes="EditorOrder(5), Limit(0, 10, 0.01f)") + float Volume = 1; /// /// The audio data format to import the audio clip as. @@ -74,10 +81,6 @@ public: BitDepth BitDepth = BitDepth::_16; String ToString() const; - - // [ISerializable] - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; }; #endif From dd3437d94cff604a7202469d71fcd024cc1d5980 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 9 Jun 2026 08:24:33 +0200 Subject: [PATCH 25/51] Fix deprecated warnings on the latest MSVC toolchain --- Source/Engine/Level/Actors/AnimatedModel.h | 2 ++ Source/Engine/Level/Actors/StaticModel.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index b6d922744..bbfa6f0ea 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -489,7 +489,9 @@ public: ModelBase* GetModel() override; bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; +PRAGMA_DISABLE_DEPRECATION_WARNINGS; bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const override; +PRAGMA_ENABLE_DEPRECATION_WARNINGS; MeshBase* GetMesh(const MeshReference& ref) const override; void UpdateBounds() override; MeshDeformation* GetMeshDeformation() const override; diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index 3a8c391f6..598b56118 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -181,7 +181,9 @@ public: ModelBase* GetModel() override; bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; +PRAGMA_DISABLE_DEPRECATION_WARNINGS; bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const override; +PRAGMA_ENABLE_DEPRECATION_WARNINGS; MeshBase* GetMesh(const MeshReference& ref) const override; MeshDeformation* GetMeshDeformation() const override; void UpdateBounds() override; From 43776d297bf62af116e565a0ba3599f4ffbb319f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 9 Jun 2026 08:24:52 +0200 Subject: [PATCH 26/51] Fix iterator include in `fmt` lib customization #4094 #4093 --- Source/ThirdParty/fmt/format.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/ThirdParty/fmt/format.h b/Source/ThirdParty/fmt/format.h index d75310ece..b9c07d274 100644 --- a/Source/ThirdParty/fmt/format.h +++ b/Source/ThirdParty/fmt/format.h @@ -37,7 +37,6 @@ #include #include #include -#include #include "core.h" @@ -464,10 +463,12 @@ FMT_INLINE void assume(bool condition) { #endif } +#if FMT_USE_ITERATOR // An approximation of iterator_t for pre-C++20 systems. template using iterator_t = decltype(std::begin(std::declval())); template using sentinel_t = decltype(std::end(std::declval())); +#endif #if FMT_USE_STRING // A workaround for std::string not having mutable data() until C++17. @@ -3407,6 +3408,7 @@ auto join(It begin, Sentinel end, string_view sep) -> join_view { return {begin, end, sep}; } +#if FMT_USE_ITERATOR /** \rst Returns a view that formats `range` with elements separated by `sep`. @@ -3428,6 +3430,7 @@ auto join(Range&& range, string_view sep) -> join_view, detail::sentinel_t> { return join(std::begin(range), std::end(range), sep); } +#endif #if FMT_USE_STRING /** From c18178e04541aa97b79138f7a9088c10266f2a57 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 9 Jun 2026 10:42:10 +0200 Subject: [PATCH 27/51] Add improvements to Gameplay Globals editing #3972 --- .../Windows/Assets/GameplayGlobalsWindow.cs | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs index 976fb75e9..a98c04a1c 100644 --- a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs +++ b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs @@ -272,7 +272,6 @@ namespace FlaxEditor.Windows.Assets { var name = e.Key; var value = _proxy.Asset.GetValue(name); - var valueContainer = new VariableValueContainer(_proxy, name, value, false); var propertyLabel = new PropertyNameLabel(name) { Tag = name, @@ -280,7 +279,15 @@ namespace FlaxEditor.Windows.Assets string tooltip = null; if (_proxy.DefaultValues.TryGetValue(name, out var defaultValue)) tooltip = "Default value: " + defaultValue; - layout.Object(propertyLabel, valueContainer, null, tooltip); + var property = layout.AddPropertyItem(propertyLabel, tooltip); + if (value == null) + { + property.Label("null"); + continue; + } + var valueContainer = new VariableValueContainer(_proxy, name, value, false); + valueContainer.SetDefaultValue(defaultValue); + property.Object(valueContainer); } } else @@ -289,19 +296,37 @@ namespace FlaxEditor.Windows.Assets { var name = e.Key; var value = e.Value; - var valueContainer = new VariableValueContainer(_proxy, name, value, true); var propertyLabel = new ClickablePropertyNameLabel(name) { Tag = name, }; propertyLabel.MouseLeftDoubleClick += (label, location) => StartParameterRenaming(name, label); propertyLabel.SetupContextMenu += OnPropertyLabelSetupContextMenu; - layout.Object(propertyLabel, valueContainer, null, "Type: " + CustomEditorsUtil.GetTypeNameUI(value.GetType())); + var tooltip = value != null ? "Type: " + CustomEditorsUtil.GetTypeNameUI(value.GetType()) : string.Empty; + var property = layout.AddPropertyItem(propertyLabel, tooltip); + if (value == null) + { + property.Label("null"); + continue; + } + var valueContainer = new VariableValueContainer(_proxy, name, value, true); + property.Object(valueContainer); + } + if (_proxy.DefaultValues.Count == 0) + { + var emptyLabel = layout.Label("Empty", TextAlignment.Center).Label; + emptyLabel.TextColor = emptyLabel.TextColorHighlighted = FlaxEngine.GUI.Style.Current.ForegroundDisabled; } - // TODO: improve the UI layout.Space(40); - var addParamType = layout.ComboBox().ComboBox; + var addPanel = layout.HorizontalPanel(); + addPanel.Panel.Size = new Float2(0, TextBox.DefaultHeight); + addPanel.Panel.Margin = Margin.Zero; + addPanel.Panel.Spacing = Utilities.Constants.UIMargin; + + addPanel.Label("New value type:"); + + var addParamType = addPanel.ComboBox().ComboBox; object lastValue = null; foreach (var e in _proxy.DefaultValues) lastValue = e.Value; @@ -314,7 +339,7 @@ namespace FlaxEditor.Windows.Assets addParamType.Items = allowedTypes; addParamType.SelectedIndex = index; _addParamType = addParamType; - var addParamButton = layout.Button("Add").Button; + var addParamButton = addPanel.Button("Add").Button; addParamButton.Clicked += OnAddParamButtonClicked; } } From 057e5684e9405b521bef28cc5f163174773826c9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 9 Jun 2026 13:49:00 +0200 Subject: [PATCH 28/51] Add Texture and Cube Texture support to Gameplay Globals --- Source/Editor/Surface/Archetypes/Tools.cs | 10 ++++-- .../Windows/Assets/GameplayGlobalsWindow.cs | 28 ++++++++++++++-- Source/Engine/Engine/GameplayGlobals.cpp | 12 +++++++ Source/Engine/Engine/GameplayGlobals.h | 1 + .../Graphics/Materials/MaterialParams.cpp | 12 ++++++- .../GPU/ParticleEmitterGraph.GPU.Textures.cpp | 10 +++--- .../Graph/GPU/ParticleEmitterGraph.GPU.h | 2 +- Source/Engine/Visject/ShaderGraph.cpp | 33 +++++++++++++++++++ .../Engine/Visject/ShaderGraphUtilities.cpp | 22 +++++++++++++ 9 files changed, 120 insertions(+), 10 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 24a0730df..b889518e7 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -651,10 +651,16 @@ namespace FlaxEditor.Surface.Archetypes foreach (var e in values) { _combobox.AddItem(e.Key); - tooltips[i++] = "Type: " + CustomEditorsUtil.GetTypeNameUI(e.Value.GetType()) + ", default value: " + e.Value; + var value = e.Value; + if (value == null) + { + tooltips[i++] = "null"; + continue; + } + tooltips[i++] = "Type: " + CustomEditorsUtil.GetTypeNameUI(value.GetType()) + ", default value: " + value; if (toSelect == e.Key) { - type = e.Value.GetType(); + type = value.GetType(); } } _combobox.Tooltips = tooltips; diff --git a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs index a98c04a1c..e4fc099a7 100644 --- a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs +++ b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs @@ -157,6 +157,7 @@ namespace FlaxEditor.Windows.Assets private void Setter(object instance, int index, object value) { + CheckForNullValue(ref value, _proxy.DefaultValues[_name].GetType()); if (_isDefault) _proxy.DefaultValues[_name] = value; else @@ -251,6 +252,8 @@ namespace FlaxEditor.Windows.Assets typeof(Rectangle), typeof(Matrix), typeof(string), + typeof(Texture), + typeof(CubeTexture), }; public override void Initialize(LayoutElementsContainer layout) @@ -282,7 +285,7 @@ namespace FlaxEditor.Windows.Assets var property = layout.AddPropertyItem(propertyLabel, tooltip); if (value == null) { - property.Label("null"); + property.Label("null").Label.TextColor = Color.Red; continue; } var valueContainer = new VariableValueContainer(_proxy, name, value, false); @@ -306,7 +309,7 @@ namespace FlaxEditor.Windows.Assets var property = layout.AddPropertyItem(propertyLabel, tooltip); if (value == null) { - property.Label("null"); + property.Label("null").Label.TextColor = Color.Red; continue; } var valueContainer = new VariableValueContainer(_proxy, name, value, true); @@ -369,6 +372,7 @@ namespace FlaxEditor.Windows.Assets Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), DefaultValue = TypeUtils.GetDefaultValue(new ScriptType(type)), }; + CheckForNullValue(ref action.DefaultValue, type); _proxy.Window.Undo.AddAction(action); action.Do(); } @@ -412,6 +416,26 @@ namespace FlaxEditor.Windows.Assets } } + private static void CheckForNullValue(ref object value, Type type) + { + if (value == null) + { + // Default values are invalid as Variant type is used in C++ to properly bind the value + if (typeof(CubeTexture).IsAssignableFrom(type)) + { + // Default cube texture + value = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.DefaultSkyCubeTexture); + } + else if (typeof(Texture).IsAssignableFrom(type)) + { + // Default texture + value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/BlackTexture"); + } + else + throw new Exception("Null values are not allowed in Gameplay Globals"); + } + } + private CustomEditorPresenter _propertiesEditor; private PropertiesProxy _proxy; private ToolStripButton _saveButton; diff --git a/Source/Engine/Engine/GameplayGlobals.cpp b/Source/Engine/Engine/GameplayGlobals.cpp index 43f18f22a..bbd69116f 100644 --- a/Source/Engine/Engine/GameplayGlobals.cpp +++ b/Source/Engine/Engine/GameplayGlobals.cpp @@ -149,6 +149,18 @@ bool GameplayGlobals::Save(const StringView& path) return false; } +void GameplayGlobals::GetReferences(Array& assets, Array& files) const +{ + BinaryAsset::GetReferences(assets, files); + + for (auto& e : Variables) + { + auto asset = (Asset*)e.Value.DefaultValue; + if (asset) + assets.Add(asset->GetID()); + } +} + #endif void GameplayGlobals::InitAsVirtual() diff --git a/Source/Engine/Engine/GameplayGlobals.h b/Source/Engine/Engine/GameplayGlobals.h index 73e02be31..220e6cae9 100644 --- a/Source/Engine/Engine/GameplayGlobals.h +++ b/Source/Engine/Engine/GameplayGlobals.h @@ -85,6 +85,7 @@ public: void InitAsVirtual() override; #if USE_EDITOR bool Save(const StringView& path = StringView::Empty) override; + void GetReferences(Array& assets, Array& files) const override; #endif protected: diff --git a/Source/Engine/Graphics/Materials/MaterialParams.cpp b/Source/Engine/Graphics/Materials/MaterialParams.cpp index 3157f3552..711fcdf2b 100644 --- a/Source/Engine/Graphics/Materials/MaterialParams.cpp +++ b/Source/Engine/Graphics/Materials/MaterialParams.cpp @@ -472,7 +472,17 @@ void MaterialParameter::Bind(BindMeta& meta) const ASSERT_LOW_LAYER(meta.Constants.Get() && meta.Constants.Length() >= (int32)(_offset + sizeof(Int4))); *((Int4*)(meta.Constants.Get() + _offset)) = (Int4)e->Value.AsInt4(); break; - default: ; + case VariantType::Asset: + { + auto texture = Cast(e->Value.AsAsset); + meta.Context->BindSR(_registerIndex, texture ? texture->GetTexture() : nullptr); + break; + } + default: +#if !BUILD_RELEASE + LOG(Warning, "Invalid Gameplay Global '{}' ({}) value type '{}' to bind to material", _name, _asAsset->GetPath(), e->Value.Type.ToString()); +#endif + break; } } } diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp index aeecef3c5..bef54d1b3 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp @@ -5,7 +5,7 @@ #include "ParticleEmitterGraph.GPU.h" #include "Engine/Graphics/Materials/MaterialInfo.h" -bool ParticleEmitterGPUGenerator::loadTexture(Node* caller, Box* box, const SerializedMaterialParam& texture, Value& result) +bool ParticleEmitterGPUGenerator::loadTexture(Node* caller, Box* box, const SerializedMaterialParam& texture, const Value& textureValue, Value& result) { ASSERT(caller && box && texture.ID.IsValid()); @@ -22,7 +22,8 @@ bool ParticleEmitterGPUGenerator::loadTexture(Node* caller, Box* box, const Seri && texture.Type != MaterialParameterType::GPUTextureVolume && texture.Type != MaterialParameterType::GPUTextureCube && texture.Type != MaterialParameterType::GPUTextureArray - && texture.Type != MaterialParameterType::CubeTexture) + && texture.Type != MaterialParameterType::CubeTexture + && textureValue.Type != VariantType::Object) { result = Value::Zero; OnError(caller, box, TEXT("No parameter for texture load or invalid type.")); @@ -41,7 +42,8 @@ bool ParticleEmitterGPUGenerator::loadTexture(Node* caller, Box* box, const Seri // Load texture const Char* format = TEXT("{0}.Load({1})"); - const String sampledValue = String::Format(format, texture.ShaderName, location.Value); + auto& shaderName = textureValue.Type == VariantType::Object ? textureValue.Value : texture.ShaderName; + const String sampledValue = String::Format(format, shaderName, location.Value); result = writeLocal(VariantType::Float4, sampledValue, parent); return false; @@ -300,7 +302,7 @@ void ParticleEmitterGPUGenerator::ProcessGroupTextures(Box* box, Node* node, Val const auto copy = *textureParam; // Load texture - loadTexture(node, box, copy, value); + loadTexture(node, box, copy, texture, value); break; } // Sample Global SDF diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h index 360f61592..62b183004 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h @@ -127,7 +127,7 @@ private: Parameter* findGraphParam(const Guid& id); bool sampleSceneTexture(Node* caller, Box* box, const SerializedMaterialParam& texture, Value& result); - bool loadTexture(Node* caller, Box* box, const SerializedMaterialParam& texture, Value& result); + bool loadTexture(Node* caller, Box* box, const SerializedMaterialParam& texture, const Value& textureValue, Value& result); void sampleSceneDepth(Node* caller, Value& value, Box* box); void linearizeSceneDepth(Node* caller, const Value& depth, Value& value); diff --git a/Source/Engine/Visject/ShaderGraph.cpp b/Source/Engine/Visject/ShaderGraph.cpp index 1df4ea439..b42b62f74 100644 --- a/Source/Engine/Visject/ShaderGraph.cpp +++ b/Source/Engine/Visject/ShaderGraph.cpp @@ -5,6 +5,8 @@ #include "ShaderGraph.h" #include "GraphUtilities.h" #include "ShaderGraphUtilities.h" +#include "Engine/Content/Assets/Texture.h" +#include "Engine/Content/Assets/CubeTexture.h" #include "Engine/Engine/GameplayGlobals.h" const Char* ShaderGenerator::_mathFunctions[] = @@ -742,6 +744,37 @@ void ShaderGenerator::ProcessGroupTools(Box* box, Node* node, Value& value) // Get param value value.Type = variable.DefaultValue.Type.Type; value.Value = param->ShaderName; + switch (variable.DefaultValue.Type.Type) + { + case VariantType::Bool: + case VariantType::Int: + case VariantType::Uint: + case VariantType::Float: + case VariantType::Float2: + case VariantType::Float3: + case VariantType::Float4: + case VariantType::Color: + case VariantType::Double2: + case VariantType::Double3: + case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: + // POD value types + break; + case VariantType::Asset: + if (Texture::GetStaticType().Fullname == variable.DefaultValue.Type.TypeName || + CubeTexture::GetStaticType().Fullname == variable.DefaultValue.Type.TypeName) + { + // Texture or Cube Texture + value.Type = VariantType::Object; + break; + } + default: + LOG(Warning, "Invalid Gameplay Global '{}' ({}) value type '{}' to bind to material", name, asset->GetPath(), variable.DefaultValue.Type.ToString()); + value = Value::Zero; + break; + } break; } // Platform Switch diff --git a/Source/Engine/Visject/ShaderGraphUtilities.cpp b/Source/Engine/Visject/ShaderGraphUtilities.cpp index bdcb55c05..4f2232904 100644 --- a/Source/Engine/Visject/ShaderGraphUtilities.cpp +++ b/Source/Engine/Visject/ShaderGraphUtilities.cpp @@ -7,6 +7,8 @@ #include "Engine/Core/Types/StringBuilder.h" #include "Engine/Core/Math/Vector4.h" #include "Engine/Content/Content.h" +#include "Engine/Content/Assets/Texture.h" +#include "Engine/Content/Assets/CubeTexture.h" #include "Engine/Engine/GameplayGlobals.h" #include "Engine/Graphics/Config.h" #include "Engine/Renderer/GlobalSignDistanceFieldPass.h" @@ -172,6 +174,26 @@ const Char* ShaderGraphUtilities::GenerateShaderResources(TextWriterUnicode& wri case MaterialParameterType::GPUTextureVolume: format = TEXT("Texture3D {0} : register(t{1});"); break; + case MaterialParameterType::GameplayGlobal: + { + auto asset = Content::LoadAsync(param.AsGuid); + if (!asset || asset->WaitForLoaded()) + break; + GameplayGlobals::Variable variable; + if (!asset->Variables.TryGet(param.Name, variable)) + break; + if (Texture::GetStaticType().Fullname == variable.DefaultValue.Type.TypeName) + { + // Texture + format = TEXT("Texture2D {0} : register(t{1});"); + } + else if (CubeTexture::GetStaticType().Fullname == variable.DefaultValue.Type.TypeName) + { + // Cube Texture + format = TEXT("TextureCube {0} : register(t{1});"); + } + break; + } case MaterialParameterType::GlobalSDF: format = TEXT("Texture3D {0}_Tex : register(t{1});\nTexture3D {0}_Mip : register(t{2});"); zeroOffset = false; From 0db92b473b3ebf32ace89e064deef3d3a8fe2b6b Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Tue, 9 Jun 2026 23:06:58 +0300 Subject: [PATCH 29/51] Fix prefab Script track remapping in scene animations Store prefab object IDs for Script tracks authored from prefab objects and mark those tracks as prefab object references so SceneAnimationPlayer can remap them for prefab instances. --- .../Editor/GUI/Timeline/Tracks/ScriptTrack.cs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Timeline/Tracks/ScriptTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ScriptTrack.cs index ee0bf5c1f..ae2e4e0c1 100644 --- a/Source/Editor/GUI/Timeline/Tracks/ScriptTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/ScriptTrack.cs @@ -52,8 +52,67 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public Script Script { - get => FlaxEngine.Object.TryFind