diff --git a/.github/ISSUE_TEMPLATE/1-bug.yaml b/.github/ISSUE_TEMPLATE/1-bug.yaml index a75003f63..7e6150557 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.yaml +++ b/.github/ISSUE_TEMPLATE/1-bug.yaml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! Please attach any minimal reproduction projects! + Thanks for taking the time to fill out this bug report! Please attach a minimal reproduction project if available! - type: textarea id: description-area attributes: @@ -17,19 +17,19 @@ body: id: steps-area attributes: label: Steps to reproduce - description: Please provide reproduction steps if possible. + description: Please provide reproduction steps if available. validations: required: true - type: dropdown id: version attributes: label: Version - description: What version of Flax are you running? + description: What version of Flax did you experience the bug in? options: - - '1.8' - '1.9' - '1.10' - '1.11' + - '1.12' - master branch default: 3 validations: diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yaml b/.github/ISSUE_TEMPLATE/2-feature-request.yaml index 338c9aea0..7ad43bf5b 100644 --- a/.github/ISSUE_TEMPLATE/2-feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yaml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out a feature request! + Thank you for taking the time to submit this feature request! - type: textarea id: description-area attributes: @@ -17,6 +17,6 @@ body: id: benefits-area attributes: label: Benefits - description: Please provide what benefits this feature would provide to the engine! + description: Please list what benefits this feature would provide to the engine! validations: required: true \ No newline at end of file diff --git a/Content/Editor/MaterialTemplates/Deformable.shader b/Content/Editor/MaterialTemplates/Deformable.shader index 688170e73..86a9200f3 100644 --- a/Content/Editor/MaterialTemplates/Deformable.shader +++ b/Content/Editor/MaterialTemplates/Deformable.shader @@ -337,7 +337,6 @@ VertexOutput VS_SplineModel(ModelInput input) // Apply world position offset per-vertex #if USE_POSITION_OFFSET output.Geometry.WorldPosition += material.PositionOffset; - output.Geometry.PrevWorldPosition += material.PositionOffset; output.Position = PROJECT_POINT(float4(output.Geometry.WorldPosition, 1), ViewProjectionMatrix); #endif diff --git a/Flax.flaxproj b/Flax.flaxproj index d6cf594a8..ca7272f0c 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 12, "Revision": 0, - "Build": 6909 + "Build": 6910 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", diff --git a/Source/Editor/Content/GUI/ContentNavigation.cs b/Source/Editor/Content/GUI/ContentNavigation.cs index a8581142b..c84b98746 100644 --- a/Source/Editor/Content/GUI/ContentNavigation.cs +++ b/Source/Editor/Content/GUI/ContentNavigation.cs @@ -19,7 +19,7 @@ namespace FlaxEditor.Content.GUI /// /// Gets the target node. /// - public ContentTreeNode TargetNode { get; } + public ContentFolderTreeNode TargetNode { get; } /// /// Initializes a new instance of the class. @@ -28,7 +28,7 @@ namespace FlaxEditor.Content.GUI /// The x position. /// The y position. /// The height. - public ContentNavigationButton(ContentTreeNode targetNode, float x, float y, float height) + public ContentNavigationButton(ContentFolderTreeNode targetNode, float x, float y, float height) : base(x, y, height) { TargetNode = targetNode; @@ -147,7 +147,7 @@ namespace FlaxEditor.Content.GUI ClearItems(); foreach (var child in Target.TargetNode.Children) { - if (child is ContentTreeNode node) + if (child is ContentFolderTreeNode node) { if (node.Folder.VisibleInHierarchy) // Respect the filter set by ContentFilterConfig.Filter(...) AddItem(node.Folder.ShortName); @@ -180,7 +180,7 @@ namespace FlaxEditor.Content.GUI var item = _items[index]; foreach (var child in Target.TargetNode.Children) { - if (child is ContentTreeNode node && node.Folder.ShortName == item) + if (child is ContentFolderTreeNode node && node.Folder.ShortName == item) { Editor.Instance.Windows.ContentWin.Navigate(node); return; diff --git a/Source/Editor/Content/Items/ContentFolder.cs b/Source/Editor/Content/Items/ContentFolder.cs index 462d0c1c1..94dbed103 100644 --- a/Source/Editor/Content/Items/ContentFolder.cs +++ b/Source/Editor/Content/Items/ContentFolder.cs @@ -59,7 +59,7 @@ namespace FlaxEditor.Content /// /// Gets the content node. /// - public ContentTreeNode Node { get; } + public ContentFolderTreeNode Node { get; } /// /// The subitems of this folder. @@ -72,7 +72,7 @@ namespace FlaxEditor.Content /// The folder type. /// The path to the item. /// The folder parent node. - internal ContentFolder(ContentFolderType type, string path, ContentTreeNode node) + internal ContentFolder(ContentFolderType type, string path, ContentFolderTreeNode node) : base(path) { FolderType = type; @@ -118,7 +118,7 @@ namespace FlaxEditor.Content get { var hasParentFolder = ParentFolder != null; - var isContentFolder = Node is MainContentTreeNode; + var isContentFolder = Node is MainContentFolderTreeNode; return hasParentFolder && !isContentFolder; } } diff --git a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs new file mode 100644 index 000000000..6741910cf --- /dev/null +++ b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs @@ -0,0 +1,433 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using FlaxEditor.GUI; +using FlaxEditor.GUI.Drag; +using FlaxEditor.GUI.Tree; +using FlaxEditor.SceneGraph; +using FlaxEditor.Utilities; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content; + +/// +/// Content folder tree node. +/// +/// +public class ContentFolderTreeNode : TreeNode +{ + private DragItems _dragOverItems; + private DragActors _dragActors; + private List _highlights; + + /// + /// The folder. + /// + protected ContentFolder _folder; + + /// + /// Whether this node can be deleted. + /// + public virtual bool CanDelete => true; + + /// + /// Whether this node can be duplicated. + /// + public virtual bool CanDuplicate => true; + + /// + /// Gets the content folder item. + /// + public ContentFolder Folder => _folder; + + /// + /// Gets the type of the folder. + /// + public ContentFolderType FolderType => _folder.FolderType; + + /// + /// Returns true if that folder can import/manage scripts. + /// + public bool CanHaveScripts => _folder.CanHaveScripts; + + /// + /// Returns true if that folder can import/manage assets. + /// + /// True if can contain assets for project, otherwise false + public bool CanHaveAssets => _folder.CanHaveAssets; + + /// + /// Gets the parent node. + /// + public ContentFolderTreeNode ParentNode => Parent as ContentFolderTreeNode; + + /// + /// Gets the folder path. + /// + public string Path => _folder.Path; + + /// + /// Gets the navigation button label. + /// + public virtual string NavButtonLabel => _folder.ShortName; + + /// + /// Initializes a new instance of the class. + /// + /// The parent node. + /// The folder path. + public ContentFolderTreeNode(ContentFolderTreeNode parent, string path) + : this(parent, parent?.FolderType ?? ContentFolderType.Other, path) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The parent node. + /// The folder type. + /// The folder path. + protected ContentFolderTreeNode(ContentFolderTreeNode parent, ContentFolderType type, string path) + : base(false, Editor.Instance.Icons.FolderClosed32, Editor.Instance.Icons.FolderOpen32) + { + _folder = new ContentFolder(type, path, this); + Text = _folder.ShortName; + if (parent != null) + { + Folder.ParentFolder = parent.Folder; + Parent = parent; + } + IconColor = Color.Transparent; // Hide default icon, we draw scaled icon manually + UpdateCustomArrowRect(); + Editor.Instance?.Windows?.ContentWin?.TryAutoExpandContentNode(this); + } + + /// + /// Updates the custom arrow rectangle so it stays aligned with the current layout. + /// + private void UpdateCustomArrowRect() + { + var contentWindow = Editor.Instance?.Windows?.ContentWin; + var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f; + var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f); + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + + // Use the current text layout, not just cached values. + var textRect = TextRect; + var iconLeft = textRect.Left - iconSize - 2.0f; + var x = Mathf.Max(iconLeft - arrowSize - 2.0f, 0.0f); + var y = Mathf.Max((HeaderHeight - arrowSize) * 0.5f, 0.0f); + + CustomArrowRect = new Rectangle(x, y, arrowSize, arrowSize); + } + + /// + public override void PerformLayout(bool force = false) + { + base.PerformLayout(force); + UpdateCustomArrowRect(); + } + + /// + /// Shows the rename popup for the item. + /// + public void StartRenaming() + { + if (!_folder.CanRename) + return; + + // Start renaming the folder + Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(false); + var dialog = RenamePopup.Show(this, TextRect, _folder.ShortName, false); + dialog.Tag = _folder; + dialog.Renamed += popup => + { + Editor.Instance.Windows.ContentWin.Rename((ContentFolder)popup.Tag, popup.Text); + Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); + }; + dialog.Closed += popup => { Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); }; + } + + /// + /// Updates the query search filter. + /// + /// The filter text. + public void UpdateFilter(string filterText) + { + bool noFilter = string.IsNullOrWhiteSpace(filterText); + + // Update itself + bool isThisVisible; + if (noFilter) + { + // Clear filter + _highlights?.Clear(); + isThisVisible = true; + } + else + { + var text = Text; + if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges)) + { + // Update highlights + if (_highlights == null) + _highlights = new List(ranges.Length); + else + _highlights.Clear(); + var style = Style.Current; + var font = style.FontSmall; + var textRect = TextRect; + for (int i = 0; i < ranges.Length; i++) + { + var start = font.GetCharPosition(text, ranges[i].StartIndex); + var end = font.GetCharPosition(text, ranges[i].EndIndex); + _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); + } + isThisVisible = true; + } + else + { + // Hide + _highlights?.Clear(); + isThisVisible = false; + } + } + + // Update children + bool isAnyChildVisible = false; + for (int i = 0; i < _children.Count; i++) + { + if (_children[i] is ContentFolderTreeNode child) + { + child.UpdateFilter(filterText); + isAnyChildVisible |= child.Visible; + } + else if (_children[i] is ContentItemTreeNode itemNode) + { + itemNode.UpdateFilter(filterText); + isAnyChildVisible |= itemNode.Visible; + } + } + + if (!noFilter) + { + bool isExpanded = isAnyChildVisible; + if (isExpanded) + Expand(true); + else + Collapse(true); + } + + Visible = isThisVisible | isAnyChildVisible; + } + + /// + public override int Compare(Control other) + { + if (other is ContentItemTreeNode) + return -1; + if (other is ContentFolderTreeNode otherNode) + return string.Compare(Text, otherNode.Text, StringComparison.Ordinal); + return base.Compare(other); + } + + /// + public override void Draw() + { + base.Draw(); + + // Draw all highlights + if (_highlights != null) + { + var style = Style.Current; + var color = style.ProgressNormal * 0.6f; + for (int i = 0; i < _highlights.Count; i++) + Render2D.FillRectangle(_highlights[i], color); + } + + var contentWindow = Editor.Instance.Windows.ContentWin; + var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f; + var icon = IsExpanded ? Editor.Instance.Icons.FolderOpen32 : Editor.Instance.Icons.FolderClosed32; + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var iconRect = new Rectangle(TextRect.Left - iconSize - 2.0f, (HeaderHeight - iconSize) * 0.5f, iconSize, iconSize); + Render2D.DrawSprite(icon, iconRect); + } + + /// + public override void OnDestroy() + { + // Delete folder item + _folder.Dispose(); + + base.OnDestroy(); + } + + /// + protected override void OnExpandedChanged() + { + base.OnExpandedChanged(); + + Editor.Instance?.Windows?.ContentWin?.OnContentTreeNodeExpandedChanged(this, IsExpanded); + } + + private DragDropEffect GetDragEffect(DragData data) + { + if (_dragActors != null && _dragActors.HasValidDrag) + return DragDropEffect.Move; + if (data is DragDataFiles) + { + if (_folder.CanHaveAssets) + return DragDropEffect.Copy; + } + else + { + if (_dragOverItems != null && _dragOverItems.HasValidDrag) + return DragDropEffect.Move; + } + + return DragDropEffect.None; + } + + private bool ValidateDragItem(ContentItem item) + { + // Reject itself and any parent + return item != _folder && !item.Find(_folder); + } + + private bool ValidateDragActors(ActorNode actor) + { + return actor.CanCreatePrefab && _folder.CanHaveAssets; + } + + private void ImportActors(DragActors actors) + { + Select(); + foreach (var actorNode in actors.Objects) + { + var actor = actorNode.Actor; + if (actors.Objects.Contains(actorNode.ParentNode as ActorNode)) + continue; + + Editor.Instance.Prefabs.CreatePrefab(actor, false); + } + Editor.Instance.Windows.ContentWin.RefreshView(); + } + + /// + protected override DragDropEffect OnDragEnterHeader(DragData data) + { + if (data is DragDataFiles) + return _folder.CanHaveAssets ? DragDropEffect.Copy : DragDropEffect.None; + + if (_dragActors == null) + _dragActors = new DragActors(ValidateDragActors); + if (_dragActors.OnDragEnter(data)) + return DragDropEffect.Move; + + if (_dragOverItems == null) + _dragOverItems = new DragItems(ValidateDragItem); + + _dragOverItems.OnDragEnter(data); + return GetDragEffect(data); + } + + /// + protected override DragDropEffect OnDragMoveHeader(DragData data) + { + if (data is DragDataFiles) + return _folder.CanHaveAssets ? DragDropEffect.Copy : DragDropEffect.None; + if (_dragActors != null && _dragActors.HasValidDrag) + return DragDropEffect.Move; + return GetDragEffect(data); + } + + /// + protected override void OnDragLeaveHeader() + { + _dragOverItems?.OnDragLeave(); + _dragActors?.OnDragLeave(); + base.OnDragLeaveHeader(); + } + + /// + protected override DragDropEffect OnDragDropHeader(DragData data) + { + var result = DragDropEffect.None; + + // Check if drop element or files + if (data is DragDataFiles files) + { + // Import files + Editor.Instance.ContentImporting.Import(files.Files, _folder); + result = DragDropEffect.Copy; + + Expand(); + } + else if (_dragActors != null && _dragActors.HasValidDrag) + { + ImportActors(_dragActors); + _dragActors.OnDragDrop(); + result = DragDropEffect.Move; + + Expand(); + } + else if (_dragOverItems != null && _dragOverItems.HasValidDrag) + { + // Move items + Editor.Instance.ContentDatabase.Move(_dragOverItems.Objects, _folder); + result = DragDropEffect.Move; + + Expand(); + } + + _dragOverItems?.OnDragDrop(); + + return result; + } + + /// + protected override void DoDragDrop() + { + DoDragDrop(DragItems.GetDragData(_folder)); + } + + /// + protected override void OnLongPress() + { + Select(); + + StartRenaming(); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (IsFocused) + { + switch (key) + { + case KeyboardKeys.F2: + StartRenaming(); + return true; + case KeyboardKeys.Delete: + if (Folder.Exists && CanDelete) + Editor.Instance.Windows.ContentWin.Delete(Folder); + return true; + } + if (RootWindow.GetKey(KeyboardKeys.Control)) + { + switch (key) + { + case KeyboardKeys.D: + if (Folder.Exists && CanDuplicate) + Editor.Instance.Windows.ContentWin.Duplicate(Folder); + return true; + } + } + } + + return base.OnKeyDown(key); + } +} diff --git a/Source/Editor/Content/Tree/ContentItemTreeNode.cs b/Source/Editor/Content/Tree/ContentItemTreeNode.cs new file mode 100644 index 000000000..09d1ec01b --- /dev/null +++ b/Source/Editor/Content/Tree/ContentItemTreeNode.cs @@ -0,0 +1,224 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using FlaxEditor.Content.GUI; +using FlaxEditor.GUI.Drag; +using FlaxEditor.GUI.Tree; +using FlaxEditor.Utilities; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content; + +/// +/// Tree node for non-folder content items. +/// +public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner +{ + private List _highlights; + + /// + /// The content item. + /// + public ContentItem Item { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The content item. + public ContentItemTreeNode(ContentItem item) + : base(false, Editor.Instance.Icons.Document128, Editor.Instance.Icons.Document128) + { + Item = item ?? throw new ArgumentNullException(nameof(item)); + UpdateDisplayedName(); + IconColor = Color.Transparent; // Reserve icon space but draw custom thumbnail. + Item.AddReference(this); + } + + private static SpriteHandle GetIcon(ContentItem item) + { + if (item == null) + return SpriteHandle.Invalid; + var icon = item.Thumbnail; + if (!icon.IsValid) + icon = item.DefaultThumbnail; + if (!icon.IsValid) + icon = Editor.Instance.Icons.Document128; + return icon; + } + + /// + /// Updates the query search filter. + /// + /// The filter text. + public void UpdateFilter(string filterText) + { + bool noFilter = string.IsNullOrWhiteSpace(filterText); + bool isVisible; + if (noFilter) + { + _highlights?.Clear(); + isVisible = true; + } + else + { + var text = Text; + if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges)) + { + if (_highlights == null) + _highlights = new List(ranges.Length); + else + _highlights.Clear(); + var style = Style.Current; + var font = style.FontSmall; + var textRect = TextRect; + for (int i = 0; i < ranges.Length; i++) + { + var start = font.GetCharPosition(text, ranges[i].StartIndex); + var end = font.GetCharPosition(text, ranges[i].EndIndex); + _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); + } + isVisible = true; + } + else + { + _highlights?.Clear(); + isVisible = false; + } + } + + Visible = isVisible; + } + + /// + public override void Draw() + { + base.Draw(); + + var icon = GetIcon(Item); + if (icon.IsValid) + { + var contentWindow = Editor.Instance.Windows.ContentWin; + var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f; + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var textRect = TextRect; + var iconRect = new Rectangle(textRect.Left - iconSize - 2.0f, (HeaderHeight - iconSize) * 0.5f, iconSize, iconSize); + Render2D.DrawSprite(icon, iconRect); + } + + if (_highlights != null) + { + var style = Style.Current; + var color = style.ProgressNormal * 0.6f; + for (int i = 0; i < _highlights.Count; i++) + Render2D.FillRectangle(_highlights[i], color); + } + } + + /// + protected override bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button) + { + if (button == MouseButton.Left) + { + Editor.Instance.Windows.ContentWin.Open(Item); + return true; + } + + return base.OnMouseDoubleClickHeader(ref location, button); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (IsFocused) + { + switch (key) + { + case KeyboardKeys.Return: + Editor.Instance.Windows.ContentWin.Open(Item); + return true; + case KeyboardKeys.F2: + Editor.Instance.Windows.ContentWin.Rename(Item); + return true; + case KeyboardKeys.Delete: + Editor.Instance.Windows.ContentWin.Delete(Item); + return true; + } + } + + return base.OnKeyDown(key); + } + + /// + protected override void DoDragDrop() + { + DoDragDrop(DragItems.GetDragData(Item)); + } + + /// + public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) + { + Item.UpdateTooltipText(); + TooltipText = Item.TooltipText; + return base.OnShowTooltip(out text, out location, out area); + } + + /// + void IContentItemOwner.OnItemDeleted(ContentItem item) + { + } + + /// + void IContentItemOwner.OnItemRenamed(ContentItem item) + { + UpdateDisplayedName(); + } + + /// + void IContentItemOwner.OnItemReimported(ContentItem item) + { + } + + /// + void IContentItemOwner.OnItemDispose(ContentItem item) + { + } + + /// + public override int Compare(Control other) + { + if (other is ContentFolderTreeNode) + return 1; + if (other is ContentItemTreeNode otherItem) + return ApplySortOrder(string.Compare(Text, otherItem.Text, StringComparison.InvariantCulture)); + return base.Compare(other); + } + + /// + public override void OnDestroy() + { + Item.RemoveReference(this); + base.OnDestroy(); + } + + /// + /// Updates the text of the node. + /// + public void UpdateDisplayedName() + { + var contentWindow = Editor.Instance?.Windows?.ContentWin; + var showExtensions = contentWindow?.View?.ShowFileExtensions ?? true; + Text = Item.ShowFileExtension || showExtensions ? Item.FileName : Item.ShortName; + } + + private static SortType GetSortType() + { + return Editor.Instance?.Windows?.ContentWin?.CurrentSortType ?? SortType.AlphabeticOrder; + } + + private static int ApplySortOrder(int result) + { + return GetSortType() == SortType.AlphabeticReverse ? -result : result; + } +} diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs deleted file mode 100644 index ca629000d..000000000 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ /dev/null @@ -1,331 +0,0 @@ -// Copyright (c) Wojciech Figat. All rights reserved. - -using System.Collections.Generic; -using FlaxEditor.GUI; -using FlaxEditor.GUI.Drag; -using FlaxEditor.GUI.Tree; -using FlaxEditor.Utilities; -using FlaxEngine; -using FlaxEngine.GUI; - -namespace FlaxEditor.Content -{ - /// - /// Content folder tree node. - /// - /// - public class ContentTreeNode : TreeNode - { - private DragItems _dragOverItems; - private List _highlights; - - /// - /// The folder. - /// - protected ContentFolder _folder; - - /// - /// Whether this node can be deleted. - /// - public virtual bool CanDelete => true; - - /// - /// Whether this node can be duplicated. - /// - public virtual bool CanDuplicate => true; - - /// - /// Gets the content folder item. - /// - public ContentFolder Folder => _folder; - - /// - /// Gets the type of the folder. - /// - public ContentFolderType FolderType => _folder.FolderType; - - /// - /// Returns true if that folder can import/manage scripts. - /// - public bool CanHaveScripts => _folder.CanHaveScripts; - - /// - /// Returns true if that folder can import/manage assets. - /// - /// True if can contain assets for project, otherwise false - public bool CanHaveAssets => _folder.CanHaveAssets; - - /// - /// Gets the parent node. - /// - public ContentTreeNode ParentNode => Parent as ContentTreeNode; - - /// - /// Gets the folder path. - /// - public string Path => _folder.Path; - - /// - /// Gets the navigation button label. - /// - public virtual string NavButtonLabel => _folder.ShortName; - - /// - /// Initializes a new instance of the class. - /// - /// The parent node. - /// The folder path. - public ContentTreeNode(ContentTreeNode parent, string path) - : this(parent, parent?.FolderType ?? ContentFolderType.Other, path) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The parent node. - /// The folder type. - /// The folder path. - protected ContentTreeNode(ContentTreeNode parent, ContentFolderType type, string path) - : base(false, Editor.Instance.Icons.FolderClosed32, Editor.Instance.Icons.FolderOpen32) - { - _folder = new ContentFolder(type, path, this); - Text = _folder.ShortName; - if (parent != null) - { - Folder.ParentFolder = parent.Folder; - Parent = parent; - } - IconColor = Style.Current.Foreground; - } - - /// - /// Shows the rename popup for the item. - /// - public void StartRenaming() - { - if (!_folder.CanRename) - return; - - // Start renaming the folder - Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(false); - var dialog = RenamePopup.Show(this, TextRect, _folder.ShortName, false); - dialog.Tag = _folder; - dialog.Renamed += popup => - { - Editor.Instance.Windows.ContentWin.Rename((ContentFolder)popup.Tag, popup.Text); - Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); - }; - dialog.Closed += popup => { Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); }; - } - - /// - /// Updates the query search filter. - /// - /// The filter text. - public void UpdateFilter(string filterText) - { - bool noFilter = string.IsNullOrWhiteSpace(filterText); - - // Update itself - bool isThisVisible; - if (noFilter) - { - // Clear filter - _highlights?.Clear(); - isThisVisible = true; - } - else - { - var text = Text; - if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges)) - { - // Update highlights - if (_highlights == null) - _highlights = new List(ranges.Length); - else - _highlights.Clear(); - var style = Style.Current; - var font = style.FontSmall; - var textRect = TextRect; - for (int i = 0; i < ranges.Length; i++) - { - var start = font.GetCharPosition(text, ranges[i].StartIndex); - var end = font.GetCharPosition(text, ranges[i].EndIndex); - _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); - } - isThisVisible = true; - } - else - { - // Hide - _highlights?.Clear(); - isThisVisible = false; - } - } - - // Update children - bool isAnyChildVisible = false; - for (int i = 0; i < _children.Count; i++) - { - if (_children[i] is ContentTreeNode child) - { - child.UpdateFilter(filterText); - isAnyChildVisible |= child.Visible; - } - } - - if (!noFilter) - { - bool isExpanded = isAnyChildVisible; - if (isExpanded) - Expand(true); - else - Collapse(true); - } - - Visible = isThisVisible | isAnyChildVisible; - } - - /// - public override void Draw() - { - base.Draw(); - - // Draw all highlights - if (_highlights != null) - { - var style = Style.Current; - var color = style.ProgressNormal * 0.6f; - for (int i = 0; i < _highlights.Count; i++) - Render2D.FillRectangle(_highlights[i], color); - } - } - - /// - public override void OnDestroy() - { - // Delete folder item - _folder.Dispose(); - - base.OnDestroy(); - } - - private DragDropEffect GetDragEffect(DragData data) - { - if (data is DragDataFiles) - { - if (_folder.CanHaveAssets) - return DragDropEffect.Copy; - } - else - { - if (_dragOverItems.HasValidDrag) - return DragDropEffect.Move; - } - - return DragDropEffect.None; - } - - private bool ValidateDragItem(ContentItem item) - { - // Reject itself and any parent - return item != _folder && !item.Find(_folder); - } - - /// - protected override DragDropEffect OnDragEnterHeader(DragData data) - { - if (_dragOverItems == null) - _dragOverItems = new DragItems(ValidateDragItem); - - _dragOverItems.OnDragEnter(data); - return GetDragEffect(data); - } - - /// - protected override DragDropEffect OnDragMoveHeader(DragData data) - { - return GetDragEffect(data); - } - - /// - protected override void OnDragLeaveHeader() - { - _dragOverItems.OnDragLeave(); - base.OnDragLeaveHeader(); - } - - /// - protected override DragDropEffect OnDragDropHeader(DragData data) - { - var result = DragDropEffect.None; - - // Check if drop element or files - if (data is DragDataFiles files) - { - // Import files - Editor.Instance.ContentImporting.Import(files.Files, _folder); - result = DragDropEffect.Copy; - - Expand(); - } - else if (_dragOverItems.HasValidDrag) - { - // Move items - Editor.Instance.ContentDatabase.Move(_dragOverItems.Objects, _folder); - result = DragDropEffect.Move; - - Expand(); - } - - _dragOverItems.OnDragDrop(); - - return result; - } - - /// - protected override void DoDragDrop() - { - DoDragDrop(DragItems.GetDragData(_folder)); - } - - /// - protected override void OnLongPress() - { - Select(); - - StartRenaming(); - } - - /// - public override bool OnKeyDown(KeyboardKeys key) - { - if (IsFocused) - { - switch (key) - { - case KeyboardKeys.F2: - StartRenaming(); - return true; - case KeyboardKeys.Delete: - if (Folder.Exists && CanDelete) - Editor.Instance.Windows.ContentWin.Delete(Folder); - return true; - } - if (RootWindow.GetKey(KeyboardKeys.Control)) - { - switch (key) - { - case KeyboardKeys.D: - if (Folder.Exists && CanDuplicate) - Editor.Instance.Windows.ContentWin.Duplicate(Folder); - return true; - } - } - } - - return base.OnKeyDown(key); - } - } -} diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index fc147f5c9..aaeed0eda 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -7,8 +7,8 @@ namespace FlaxEditor.Content /// /// Content tree node used for main directories. /// - /// - public class MainContentTreeNode : ContentTreeNode + /// + public class MainContentFolderTreeNode : ContentFolderTreeNode { private FileSystemWatcher _watcher; @@ -19,12 +19,12 @@ namespace FlaxEditor.Content public override bool CanDuplicate => false; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent project. /// The folder type. /// The folder path. - public MainContentTreeNode(ProjectTreeNode parent, ContentFolderType type, string path) + public MainContentFolderTreeNode(ProjectFolderTreeNode parent, ContentFolderType type, string path) : base(parent, type, path) { _watcher = new FileSystemWatcher(path) diff --git a/Source/Editor/Content/Tree/ProjectTreeNode.cs b/Source/Editor/Content/Tree/ProjectTreeNode.cs index 61002d4fa..4f921e371 100644 --- a/Source/Editor/Content/Tree/ProjectTreeNode.cs +++ b/Source/Editor/Content/Tree/ProjectTreeNode.cs @@ -1,5 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using FlaxEngine.GUI; namespace FlaxEditor.Content @@ -7,8 +8,8 @@ namespace FlaxEditor.Content /// /// Root tree node for the project workspace. /// - /// - public sealed class ProjectTreeNode : ContentTreeNode + /// + public sealed class ProjectFolderTreeNode : ContentFolderTreeNode { /// /// The project/ @@ -18,18 +19,18 @@ namespace FlaxEditor.Content /// /// The project content directory. /// - public MainContentTreeNode Content; + public MainContentFolderTreeNode Content; /// /// The project source code directory. /// - public MainContentTreeNode Source; + public MainContentFolderTreeNode Source; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The project. - public ProjectTreeNode(ProjectInfo project) + public ProjectFolderTreeNode(ProjectInfo project) : base(null, project.ProjectFolderPath) { Project = project; @@ -48,9 +49,29 @@ namespace FlaxEditor.Content /// public override int Compare(Control other) { - // Move the main game project to the top - if (Project.Name == Editor.Instance.GameProject.Name) - return -1; + if (other is ProjectFolderTreeNode otherProject) + { + var gameProject = Editor.Instance.GameProject; + var engineProject = Editor.Instance.EngineProject; + bool isGame = Project == gameProject; + bool isEngine = Project == engineProject; + bool otherIsGame = otherProject.Project == gameProject; + bool otherIsEngine = otherProject.Project == engineProject; + + // Main game project at the top + if (isGame && !otherIsGame) + return -1; + if (!isGame && otherIsGame) + return 1; + + // Engine project at the bottom (when distinct) + if (isEngine && !otherIsEngine) + return 1; + if (!isEngine && otherIsEngine) + return -1; + + return string.CompareOrdinal(Project.Name, otherProject.Project.Name); + } return base.Compare(other); } } diff --git a/Source/Editor/Content/Tree/RootContentTreeNode.cs b/Source/Editor/Content/Tree/RootContentTreeNode.cs index d9c1ba761..d1f816c14 100644 --- a/Source/Editor/Content/Tree/RootContentTreeNode.cs +++ b/Source/Editor/Content/Tree/RootContentTreeNode.cs @@ -5,13 +5,13 @@ namespace FlaxEditor.Content /// /// Root tree node for the content workspace. /// - /// - public sealed class RootContentTreeNode : ContentTreeNode + /// + public sealed class RootContentFolderTreeNode : ContentFolderTreeNode { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public RootContentTreeNode() + public RootContentFolderTreeNode() : base(null, string.Empty) { } diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index d312f64b2..def57b332 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -55,9 +55,8 @@ namespace FlaxEditor.CustomEditors.Dedicated { // TODO: consider editing more than one instance of the same prefab asset at once - var prefab = FlaxEngine.Content.LoadAsync(actor.PrefabID); - // TODO: don't stall here? - if (prefab && !prefab.WaitForLoaded()) + var prefab = FlaxEngine.Content.Load(actor.PrefabID); + if (prefab) { var prefabObjectId = actor.PrefabObjectID; var prefabInstance = prefab.GetDefaultInstance(ref prefabObjectId); @@ -203,7 +202,7 @@ namespace FlaxEditor.CustomEditors.Dedicated //Presenter.BuildLayoutOnUpdate(); // Better way is to just update the reference value using the new default instance of the prefab, created after changes apply - if (Values != null && prefab && !prefab.WaitForLoaded()) + if (Values != null && (Actor)Values[0] && prefab && !prefab.WaitForLoaded()) { var actor = (Actor)Values[0]; var prefabObjectId = actor.PrefabObjectID; diff --git a/Source/Editor/GUI/NavigationBar.cs b/Source/Editor/GUI/NavigationBar.cs index eb87eaafc..7f43d09ac 100644 --- a/Source/Editor/GUI/NavigationBar.cs +++ b/Source/Editor/GUI/NavigationBar.cs @@ -76,9 +76,23 @@ namespace FlaxEditor.GUI toolstrip.IsLayoutLocked = toolstripLocked; toolstrip.ItemsMargin = toolstripMargin; - var lastToolstripButton = toolstrip.LastButton; + var margin = toolstrip.ItemsMargin; + float xOffset = margin.Left; + bool hadChild = false; + for (int i = 0; i < toolstrip.ChildrenCount; i++) + { + var child = toolstrip.GetChild(i); + if (child == this || !child.Visible) + continue; + hadChild = true; + xOffset += child.Width + margin.Width; + } + + var right = hadChild ? xOffset - margin.Width : margin.Left; var parentSize = Parent.Size; - Bounds = new Rectangle(lastToolstripButton.Right + 8.0f, 0, parentSize.X - X - 8.0f, toolstrip.Height); + var x = right + 8.0f; + var width = Mathf.Max(parentSize.X - x - 8.0f, 0.0f); + Bounds = new Rectangle(x, 0, width, toolstrip.Height); } /// diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index babebbd5b..b76cd0607 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System.Collections.Generic; +using FlaxEditor.Options; using FlaxEditor.Viewport; using FlaxEngine; using FlaxEngine.GUI; @@ -10,13 +11,20 @@ namespace FlaxEditor.Gizmo; [HideInEditor] internal class DirectionGizmo : ContainerControl { + public const float DefaultGizmoSize = 112.5f; + + private const float AxisLength = 71.25f; + private const float SpriteRadius = 10.85f; + private IGizmoOwner _owner; private ViewportProjection _viewportProjection; private EditorViewport _viewport; private Vector3 _gizmoCenter; - private float _axisLength = 75.0f; - private float _textAxisLength = 95.0f; - private float _spriteRadius = 12.0f; + private float _gizmoBrightness; + private float _gizmoOpacity; + private float _backgroundOpacity; + private float _axisLength; + private float _spriteRadius; private AxisData _xAxisData; private AxisData _yAxisData; @@ -92,19 +100,34 @@ internal class DirectionGizmo : ContainerControl _xAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "X", AxisColor = new Color(1.0f, 0.0f, 0.02745f, 1.0f), Negative = false, Direction = AxisDirection.PosX }; _yAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Y", AxisColor = new Color(0.239215f, 1.0f, 0.047058f, 1.0f), Negative = false, Direction = AxisDirection.PosY }; - _zAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Z", AxisColor = new Color(0.0f, 0.0235294f, 1.0f, 1.0f), Negative = false, Direction = AxisDirection.PosZ }; + _zAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Z", AxisColor = new Color(0.0f, 0.3607f, 0.9f, 1.0f), Negative = false, Direction = AxisDirection.PosZ }; _negXAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-X", AxisColor = new Color(1.0f, 0.0f, 0.02745f, 1.0f), Negative = true, Direction = AxisDirection.NegX }; _negYAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Y", AxisColor = new Color(0.239215f, 1.0f, 0.047058f, 1.0f), Negative = true, Direction = AxisDirection.NegY }; - _negZAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Z", AxisColor = new Color(0.0f, 0.0235294f, 1.0f, 1.0f), Negative = true, Direction = AxisDirection.NegZ }; + _negZAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Z", AxisColor = new Color(0.0f, 0.3607f, 0.9f, 1.0f), Negative = true, Direction = AxisDirection.NegZ }; _axisData.EnsureCapacity(6); _spritePositions.EnsureCapacity(6); - _posHandle = Editor.Instance.Icons.VisjectBoxClosed32; - _negHandle = Editor.Instance.Icons.VisjectBoxOpen32; + var editor = Editor.Instance; + _posHandle = editor.Icons.VisjectBoxClosed32; + _negHandle = editor.Icons.VisjectBoxOpen32; _fontReference = new FontReference(Style.Current.FontSmall); - _fontReference.Size = 8; + + editor.Options.OptionsChanged += OnEditorOptionsChanged; + OnEditorOptionsChanged(editor.Options.Options); + } + + private void OnEditorOptionsChanged(EditorOptions options) + { + float gizmoScale = options.Viewport.DirectionGizmoScale; + _axisLength = AxisLength * gizmoScale; + _spriteRadius = SpriteRadius * gizmoScale; + _gizmoBrightness = options.Viewport.DirectionGizmoBrightness; + _gizmoOpacity = options.Viewport.DirectionGizmoOpacity; + _backgroundOpacity = options.Viewport.DirectionGizmoBackgroundOpacity; + + _fontReference.Size = 8.25f * gizmoScale; } private bool IsPointInSprite(Float2 point, Float2 spriteCenter) @@ -134,9 +157,9 @@ internal class DirectionGizmo : ContainerControl } /// - public override bool OnMouseDown(Float2 location, MouseButton button) + public override bool OnMouseUp(Float2 location, MouseButton button) { - if (base.OnMouseDown(location, button)) + if (base.OnMouseUp(location, button)) return true; // Check which axis is being clicked - check from closest to farthest for proper layering @@ -156,12 +179,12 @@ internal class DirectionGizmo : ContainerControl { Quaternion orientation = direction switch { - AxisDirection.PosX => Quaternion.Euler(0, 90, 0), - AxisDirection.NegX => Quaternion.Euler(0, -90, 0), - AxisDirection.PosY => Quaternion.Euler(-90, 0, 0), - AxisDirection.NegY => Quaternion.Euler(90, 0, 0), - AxisDirection.PosZ => Quaternion.Euler(0, 0, 0), - AxisDirection.NegZ => Quaternion.Euler(0, 180, 0), + AxisDirection.PosX => Quaternion.Euler(0, -90, 0), + AxisDirection.NegX => Quaternion.Euler(0, 90, 0), + AxisDirection.PosY => Quaternion.Euler(90, 0, 0), + AxisDirection.NegY => Quaternion.Euler(-90, 0, 0), + AxisDirection.PosZ => Quaternion.Euler(0, 180, 0), + AxisDirection.NegZ => Quaternion.Euler(0, 0, 0), _ => Quaternion.Identity }; _viewport.OrientViewport(ref orientation); @@ -192,8 +215,19 @@ internal class DirectionGizmo : ContainerControl // Normalize by viewport height to keep size independent of FOV and viewport dimensions float heightNormalization = _viewport.Height / 720.0f; // 720 = reference height + + // Fix in axes distance no matter FOV/OrthoScale to keep consistent size regardless of zoom level if (_owner.Viewport.UseOrthographicProjection) - heightNormalization /= _owner.Viewport.OrthographicScale * 0.5f; // Fix in ortho view to keep consistent size regardless of zoom level + heightNormalization /= _owner.Viewport.OrthographicScale * 0.5f; + else + { + // This could be some actual math expression, not that hack + var fov = _owner.Viewport.FieldOfView / 60.0f; + float scaleAt30 = 0.1f, scaleAt60 = 1.0f, scaleAt120 = 1.5f, scaleAt180 = 3.0f; + heightNormalization /= Mathf.Lerp(scaleAt30, scaleAt60, fov); + heightNormalization /= Mathf.Lerp(scaleAt60, scaleAt120, Mathf.Saturate(fov - 1)); + heightNormalization /= Mathf.Lerp(scaleAt60, scaleAt180, Mathf.Saturate(fov - 2)); + } Float2 xDelta = (xProjected - gizmoCenterScreen) / heightNormalization; Float2 yDelta = (yProjected - gizmoCenterScreen) / heightNormalization; @@ -232,33 +266,32 @@ internal class DirectionGizmo : ContainerControl // Rebuild sprite positions list for hover detection _spritePositions.Clear(); - Render2D.DrawSprite(_posHandle, new Rectangle(0, 0, Size), Color.Black.AlphaMultiplied(0.1f)); + Render2D.DrawSprite(_posHandle, new Rectangle(0, 0, Size), Color.Black.AlphaMultiplied(_backgroundOpacity)); // Draw in order from farthest to closest for (int i = 0; i < _axisData.Count; i++) { var axis = _axisData[i]; Float2 tipScreen = relativeCenter + axis.Delta * _axisLength; - Float2 tipTextScreen = relativeCenter + axis.Delta * _textAxisLength; bool isHovered = _hoveredAxisIndex == i; // Store sprite position for hover detection - _spritePositions.Add((tipTextScreen, axis.Direction)); + _spritePositions.Add((tipScreen, axis.Direction)); var axisColor = isHovered ? new Color(1.0f, 0.8980392f, 0.039215688f) : axis.AxisColor; + axisColor = axisColor.RGBMultiplied(_gizmoBrightness).AlphaMultiplied(_gizmoOpacity); var font = _fontReference.GetFont(); if (!axis.Negative) { - Render2D.DrawLine(relativeCenter, tipScreen, axisColor, 2.0f); - Render2D.DrawSprite(_posHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); - Render2D.DrawText(font, axis.Label, isHovered ? Color.Gray : Color.Black, tipTextScreen - font.MeasureText(axis.Label) * 0.5f); + Render2D.DrawLine(relativeCenter, tipScreen, axisColor, 1.5f); + Render2D.DrawSprite(_posHandle, new Rectangle(tipScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); + Render2D.DrawText(font, axis.Label, Color.Black, tipScreen - font.MeasureText(axis.Label) * 0.5f); } else { - Render2D.DrawSprite(_posHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor.RGBMultiplied(0.65f)); - Render2D.DrawSprite(_negHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); + Render2D.DrawSprite(_posHandle, new Rectangle(tipScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor.RGBMultiplied(0.85f).AlphaMultiplied(0.8f)); if (isHovered) - Render2D.DrawText(font, axis.Label, Color.Black, tipTextScreen - font.MeasureText(axis.Label) * 0.5f); + Render2D.DrawText(font, axis.Label, Color.Black, tipScreen - font.MeasureText(axis.Label) * 0.5f); } } diff --git a/Source/Editor/Gizmo/UIEditorGizmo.cs b/Source/Editor/Gizmo/UIEditorGizmo.cs index 61109556c..77e6fff9d 100644 --- a/Source/Editor/Gizmo/UIEditorGizmo.cs +++ b/Source/Editor/Gizmo/UIEditorGizmo.cs @@ -529,7 +529,7 @@ namespace FlaxEditor if (EnableBackground && _view != null) { // Draw background - Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Width, Height); + Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Size, _view.Location); if (ShowGrid) { diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 57982f8c3..6e4dd7c25 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -24,22 +24,22 @@ namespace FlaxEditor.Modules private bool _rebuildInitFlag; private int _itemsCreated; private int _itemsDeleted; - private readonly HashSet _dirtyNodes = new HashSet(); + private readonly HashSet _dirtyNodes = new HashSet(); /// /// The project directory. /// - public ProjectTreeNode Game { get; private set; } + public ProjectFolderTreeNode Game { get; private set; } /// /// The engine directory. /// - public ProjectTreeNode Engine { get; private set; } + public ProjectFolderTreeNode Engine { get; private set; } /// /// The list of all projects workspace directories (including game, engine and plugins projects). /// - public readonly List Projects = new List(); + public readonly List Projects = new List(); /// /// The list with all content items proxy objects. Use and to modify this or to refresh database when adding new item proxy types. @@ -116,7 +116,7 @@ namespace FlaxEditor.Modules /// /// The project. /// The project workspace or null if not loaded into database. - public ProjectTreeNode GetProjectWorkspace(ProjectInfo project) + public ProjectFolderTreeNode GetProjectWorkspace(ProjectInfo project) { return Projects.FirstOrDefault(x => x.Project == project); } @@ -878,7 +878,7 @@ namespace FlaxEditor.Modules } } - private void LoadFolder(ContentTreeNode node, bool checkSubDirs) + private void LoadFolder(ContentFolderTreeNode node, bool checkSubDirs) { if (node == null) return; @@ -957,7 +957,7 @@ namespace FlaxEditor.Modules if (childFolderNode == null) { // Create node - ContentTreeNode n = new ContentTreeNode(node, childPath); + ContentFolderTreeNode n = new ContentFolderTreeNode(node, childPath); if (!_isDuringFastSetup) sortChildren = true; @@ -982,7 +982,7 @@ namespace FlaxEditor.Modules node.SortChildren(); // Ignore some special folders - if (node is MainContentTreeNode mainNode && mainNode.Folder.ShortName == "Source") + if (node is MainContentFolderTreeNode mainNode && mainNode.Folder.ShortName == "Source") { var mainNodeChild = mainNode.Folder.Find(StringUtils.CombinePaths(mainNode.Path, "obj")) as ContentFolder; if (mainNodeChild != null) @@ -999,7 +999,7 @@ namespace FlaxEditor.Modules } } - private void LoadScripts(ContentTreeNode parent, string[] files) + private void LoadScripts(ContentFolderTreeNode parent, string[] files) { for (int i = 0; i < files.Length; i++) { @@ -1045,7 +1045,7 @@ namespace FlaxEditor.Modules } } - private void LoadAssets(ContentTreeNode parent, string[] files) + private void LoadAssets(ContentFolderTreeNode parent, string[] files) { for (int i = 0; i < files.Length; i++) { @@ -1097,20 +1097,20 @@ namespace FlaxEditor.Modules var workspace = GetProjectWorkspace(project); if (workspace == null) { - workspace = new ProjectTreeNode(project); + workspace = new ProjectFolderTreeNode(project); Projects.Add(workspace); var contentFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Content"); if (Directory.Exists(contentFolder)) { - workspace.Content = new MainContentTreeNode(workspace, ContentFolderType.Content, contentFolder); + workspace.Content = new MainContentFolderTreeNode(workspace, ContentFolderType.Content, contentFolder); workspace.Content.Folder.ParentFolder = workspace.Folder; } var sourceFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Source"); if (Directory.Exists(sourceFolder)) { - workspace.Source = new MainContentTreeNode(workspace, ContentFolderType.Source, sourceFolder); + workspace.Source = new MainContentFolderTreeNode(workspace, ContentFolderType.Source, sourceFolder); workspace.Source.Folder.ParentFolder = workspace.Folder; } } @@ -1218,16 +1218,16 @@ namespace FlaxEditor.Modules Proxy.Add(new GenericJsonAssetProxy()); // Create content folders nodes - Engine = new ProjectTreeNode(Editor.EngineProject) + Engine = new ProjectFolderTreeNode(Editor.EngineProject) { - Content = new MainContentTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder), + Content = new MainContentFolderTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder), }; if (Editor.GameProject != Editor.EngineProject) { - Game = new ProjectTreeNode(Editor.GameProject) + Game = new ProjectFolderTreeNode(Editor.GameProject) { - Content = new MainContentTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder), - Source = new MainContentTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder), + Content = new MainContentFolderTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder), + Source = new MainContentFolderTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder), }; // TODO: why it's required? the code above should work for linking the nodes hierarchy Game.Content.Folder.ParentFolder = Game.Folder; @@ -1307,7 +1307,7 @@ namespace FlaxEditor.Modules } } - internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e) + internal void OnDirectoryEvent(MainContentFolderTreeNode node, FileSystemEventArgs e) { // Ensure to be ready for external events if (_isDuringFastSetup) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index aed633672..c7f6ba544 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -171,5 +171,40 @@ namespace FlaxEditor.Options [DefaultValue(1000.0f), Limit(0.0f, 20000.0f, 5.0f)] [EditorDisplay("Viewport Icons"), EditorOrder(410)] public float MaxSizeDistance { get; set; } = 1000.0f; + + /// + /// Gets or sets a value that indicates whether the main viewports is visible. + /// + [DefaultValue(true)] + [EditorDisplay("Direction Gizmo"), EditorOrder(500), Tooltip("Sets the visibility of the direction gizmo in the main editor viewport.")] + public bool ShowDirectionGizmo { get; set; } = true; + + /// + /// Gets or sets a value by which the main viewports size is multiplied with. + /// + [DefaultValue(1f), Limit(0.0f, 2.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(501), Tooltip("The scale of the direction gizmo in the main viewport.")] + public float DirectionGizmoScale { get; set; } = 1f; + + /// + /// Gets or sets a value for the opacity of the main viewports background. + /// + [DefaultValue(0.1f), Limit(0.0f, 1.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(502), Tooltip("The background opacity of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoBackgroundOpacity { get; set; } = 0.1f; + + /// + /// Gets or sets a value for the opacity of the main viewports . + /// + [DefaultValue(0.6f), Limit(0.0f, 1.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(503), Tooltip("The opacity of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoOpacity { get; set; } = 0.6f; + + /// + /// Gets or sets a value for the opacity of the main viewports . + /// + [DefaultValue(1f), Limit(0.0f, 2.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(504), Tooltip("The brightness of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoBrightness{ get; set; } = 1f; } } diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index 450960af7..cb8681be0 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -102,8 +102,14 @@ namespace FlaxEditor.Surface.Archetypes outline = style.BorderHighlighted; else if (_editor._node.SelectedAnimationIndex == _index) outline = style.BackgroundSelected; + + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + Render2D.DrawSprite(icon, rect.MakeExpanded(4.0f), outline); Render2D.DrawSprite(icon, rect, style.Foreground); + + Render2D.Features = features; } /// @@ -563,6 +569,9 @@ namespace FlaxEditor.Surface.Archetypes // Grid _node.DrawEditorGrid(ref rect); + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + base.Draw(); // Draw debug position @@ -578,6 +587,8 @@ namespace FlaxEditor.Surface.Archetypes Render2D.DrawSprite(icon, debugRect, style.ProgressNormal); } + Render2D.Features = features; + // Frame var frameColor = containsFocus ? style.BackgroundSelected : (IsMouseOver ? style.ForegroundGrey : style.ForegroundDisabled); Render2D.DrawRectangle(new Rectangle(1, 1, rect.Width - 2, rect.Height - 2), frameColor); diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index e66f38398..b4afff091 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -79,7 +79,7 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch) { var marginX = FlaxEditor.Surface.Constants.NodeMarginX; - var uiStartPosY = FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize; + var uiStartPosY = FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight; var editButton = new Button(marginX, uiStartPosY, 246, 20) { diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index 9f3112905..410f6d33f 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -70,6 +70,9 @@ namespace FlaxEditor.Surface.Archetypes if (box.ID != _assetBox.ID) return; _assetSelect.Visible = !box.HasAnyConnection; + + if (!Archetype.Flags.HasFlag(NodeFlags.FixedSize)) + ResizeAuto(); } } @@ -243,8 +246,8 @@ namespace FlaxEditor.Surface.Archetypes { Type = NodeElementType.Input, Position = new Float2( - FlaxEditor.Surface.Constants.NodeMarginX - FlaxEditor.Surface.Constants.BoxOffsetX, - FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + ylevel * FlaxEditor.Surface.Constants.LayoutOffsetY), + FlaxEditor.Surface.Constants.NodeMarginX, + FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + ylevel * FlaxEditor.Surface.Constants.LayoutOffsetY), Text = "Pose " + _blendPoses.Count, Single = true, ValueIndex = -1, @@ -263,7 +266,7 @@ namespace FlaxEditor.Surface.Archetypes private void UpdateHeight() { float nodeHeight = 10 + (Mathf.Max(_blendPoses.Count, 1) + 3) * FlaxEditor.Surface.Constants.LayoutOffsetY; - Height = nodeHeight + FlaxEditor.Surface.Constants.NodeMarginY * 2 + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.NodeFooterSize; + Height = nodeHeight + FlaxEditor.Surface.Constants.NodeMarginY * 2 + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.NodeFooterSize; } /// @@ -621,8 +624,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new MultiBlend1D(id, context, arch, groupArch), Title = "Multi Blend 1D", Description = "Animation blending in 1D", - Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize, - Size = new Float2(420, 300), + Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize | NodeFlags.FixedSize, + Size = new Float2(420, 320), DefaultValues = new object[] { // Node data @@ -646,9 +649,9 @@ namespace FlaxEditor.Surface.Archetypes // Axis X NodeElementArchetype.Factory.Input(3, "X", true, typeof(float), 4), - NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY + 2, "(min: max: )"), - NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY + 2, 0), - NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY + 2, 0), + NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY, "(min: max: )"), + NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY, 0), + NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY, 0), } }, new NodeArchetype @@ -657,8 +660,8 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new MultiBlend2D(id, context, arch, groupArch), Title = "Multi Blend 2D", Description = "Animation blending in 2D", - Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize, - Size = new Float2(420, 620), + Flags = NodeFlags.AnimGraph | NodeFlags.VariableValuesSize | NodeFlags.FixedSize, + Size = new Float2(420, 640), DefaultValues = new object[] { // Node data @@ -682,15 +685,15 @@ namespace FlaxEditor.Surface.Archetypes // Axis X NodeElementArchetype.Factory.Input(3, "X", true, typeof(float), 4), - NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY + 2, "(min: max: )"), - NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY + 2, 0), - NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY + 2, 0), + NodeElementArchetype.Factory.Text(30, 3 * Surface.Constants.LayoutOffsetY, "(min: max: )"), + NodeElementArchetype.Factory.Vector_X(60, 3 * Surface.Constants.LayoutOffsetY, 0), + NodeElementArchetype.Factory.Vector_Y(145, 3 * Surface.Constants.LayoutOffsetY, 0), // Axis Y NodeElementArchetype.Factory.Input(4, "Y", true, typeof(float), 5), - NodeElementArchetype.Factory.Text(30, 4 * Surface.Constants.LayoutOffsetY + 2, "(min: max: )"), - NodeElementArchetype.Factory.Vector_Z(60, 4 * Surface.Constants.LayoutOffsetY + 2, 0), - NodeElementArchetype.Factory.Vector_W(145, 4 * Surface.Constants.LayoutOffsetY + 2, 0), + NodeElementArchetype.Factory.Text(30, 4 * Surface.Constants.LayoutOffsetY, "(min: max: )"), + NodeElementArchetype.Factory.Vector_Z(60, 4 * Surface.Constants.LayoutOffsetY, 0), + NodeElementArchetype.Factory.Vector_W(145, 4 * Surface.Constants.LayoutOffsetY, 0), } }, new NodeArchetype @@ -931,7 +934,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 27, Title = "Copy Node", Description = "Copies the skeleton node transformation data (in local space)", - Flags = NodeFlags.AnimGraph, + Flags = NodeFlags.AnimGraph | NodeFlags.FixedSize, Size = new Float2(260, 140), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 0b4b3f713..cdc223a8d 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -189,13 +189,69 @@ namespace FlaxEditor.Surface.Archetypes public override void Draw() { - base.Draw(); + var style = Style.Current; + var backgroundRect = new Rectangle(Float2.Zero, Size); + + // Shadow + if (DrawBasicShadow) + { + var shadowRect = backgroundRect.MakeOffsetted(ShadowOffset); + Render2D.FillRectangle(shadowRect, Color.Black.AlphaMultiplied(0.125f)); + } + + // Background + Render2D.FillRectangle(backgroundRect, BackgroundColor); + + // Breakpoint hit + if (Breakpoint.Hit) + { + var colorTop = Color.OrangeRed; + var colorBottom = Color.Red; + var time = DateTime.Now - Engine.StartupTime; + Render2D.DrawRectangle(backgroundRect.MakeExpanded(Mathf.Lerp(3.0f, 12.0f, Mathf.Sin((float)time.TotalSeconds * 10.0f) * 0.5f + 0.5f)), colorTop, colorTop, colorBottom, colorBottom, 2.0f); + } + + // Header + var headerColor = style.BackgroundHighlighted; + if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting) + headerColor *= 1.07f; + Render2D.FillRectangle(_headerRect, style.BackgroundHighlighted); + Render2D.DrawText(style.FontLarge, Title, _headerTextRect, style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, FlaxEditor.Surface.Constants.NodeHeaderTextScale); + + // Close button + if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) + { + bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting; + DrawCloseButton(_closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); + } + + DrawChildren(); + + // Selection outline + if (_isSelected) + { + var colorTop = Color.Orange; + var colorBottom = Color.OrangeRed; + Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom, 2.5f); + } + + // Breakpoint dot + if (Breakpoint.Set) + { + var icon = Breakpoint.Enabled ? Surface.Style.Icons.BoxClose : Surface.Style.Icons.BoxOpen; + Render2D.DrawSprite(icon, new Rectangle(-7, -7, 16, 16), new Color(0.9f, 0.9f, 0.9f)); + Render2D.DrawSprite(icon, new Rectangle(-6, -6, 14, 14), new Color(0.894117647f, 0.0784313725f, 0.0f)); + } + + if (highlightBox != null) + Render2D.DrawRectangle(highlightBox.Bounds, style.BorderHighlighted, 2f); // Debug Info if (!string.IsNullOrEmpty(_debugInfo)) { - var style = Style.Current; - Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 4, _debugInfoSize), style.Foreground); + // Draw an extra background to cover the archetype color colored node background and make text more legible + Render2D.FillRectangle(new Rectangle(0, _headerRect.Bottom + 4, Width, Height - _headerRect.Bottom - 4), style.BackgroundHighlighted); + Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 7, _debugInfoSize), style.Foreground, scale: 0.8f); } // Debug relevancy outline @@ -203,7 +259,7 @@ namespace FlaxEditor.Surface.Archetypes { var colorTop = Color.LightYellow; var colorBottom = Color.Yellow; - var backgroundRect = new Rectangle(Float2.One, Size - new Float2(2.0f)); + backgroundRect = new Rectangle(Float2.One, Size - new Float2(2.0f)); Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom); } } @@ -415,8 +471,8 @@ namespace FlaxEditor.Surface.Archetypes // Setup boxes _input = (InputBox)GetBox(0); _output = (OutputBox)GetBox(1); - _input.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxSize * -0.5f); - _output.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxSize * 0.5f); + _input.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxRowHeight * -0.5f); + _output.ConnectionOffset = new Float2(0, FlaxEditor.Surface.Constants.BoxRowHeight * 0.5f); // Setup node type and data var flagsRoot = NodeFlags.NoRemove | NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaPaste; @@ -515,7 +571,7 @@ namespace FlaxEditor.Surface.Archetypes height += decorator.Height + DecoratorsMarginY; width = Mathf.Max(width, decorator.Width - FlaxEditor.Surface.Constants.NodeCloseButtonSize - 2 * DecoratorsMarginX); } - Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.NodeFooterSize); + Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderHeight); UpdateRectangles(); } @@ -536,13 +592,13 @@ namespace FlaxEditor.Surface.Archetypes if (decorator.IndexInParent < indexInParent) decorator.IndexInParent = indexInParent + 1; // Push elements above the node } - const float footerSize = FlaxEditor.Surface.Constants.NodeFooterSize; - const float headerSize = FlaxEditor.Surface.Constants.NodeHeaderSize; + + const float headerHeight = FlaxEditor.Surface.Constants.NodeHeaderHeight; const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; - _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerSize); + float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.75f; + _headerRect = new Rectangle(0, bounds.Y - Y, bounds.Width, headerHeight); + _headerTextRect = _headerRect with { X = 5f, Width = Width - closeButtonSize - closeButtonMargin * 4f }; _closeButtonRect = new Rectangle(bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); - _footerRect = new Rectangle(0, bounds.Height - footerSize, bounds.Width, footerSize); if (_output != null && _output.Visible) { _footerRect.Y -= ConnectionAreaHeight; @@ -648,6 +704,8 @@ namespace FlaxEditor.Surface.Archetypes private DragDecorator _dragDecorator; private float _dragLocation = -1; + internal override bool DrawBasicShadow => false; + internal Decorator(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { @@ -667,7 +725,7 @@ namespace FlaxEditor.Surface.Archetypes } } - protected override Color FooterColor => Color.Transparent; + protected override Color ArchetypeColor => Color.Transparent; protected override Float2 CalculateNodeSize(float width, float height) { @@ -676,7 +734,7 @@ namespace FlaxEditor.Surface.Archetypes width = Mathf.Max(width, _debugInfoSize.X + 8.0f); height += _debugInfoSize.Y + 8.0f; } - return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2 + DecoratorsMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderSize); + return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2 + DecoratorsMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderHeight); } protected override void UpdateRectangles() @@ -684,8 +742,14 @@ namespace FlaxEditor.Surface.Archetypes base.UpdateRectangles(); _footerRect = Rectangle.Empty; + const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; + float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize * 0.75f; + _closeButtonRect = new Rectangle(Bounds.Width - closeButtonSize - closeButtonMargin, _headerRect.Y + closeButtonMargin, closeButtonSize, closeButtonSize); if (_dragIcon != null) - _dragIcon.Bounds = new Rectangle(_closeButtonRect.X - _closeButtonRect.Width, _closeButtonRect.Y, _closeButtonRect.Size); + { + var dragIconRect = _closeButtonRect.MakeExpanded(5f); + _dragIcon.Bounds = new Rectangle(dragIconRect.X - dragIconRect.Width, dragIconRect.Y, dragIconRect.Size); + } } protected override void UpdateTitle() @@ -754,16 +818,21 @@ namespace FlaxEditor.Surface.Archetypes { base.OnSurfaceLoaded(action); + var node = Node; if (action == SurfaceNodeActions.Undo) { // Update parent node layout when restoring decorator from undo - var node = Node; if (node != null) { node._decorators = null; node.ResizeAuto(); } } + else + { + // Correctly size decorators when surface is loaded + node?.ResizeAuto(); + } } /// diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index bcf0159d2..32baadc1c 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -173,10 +173,10 @@ namespace FlaxEditor.Surface.Archetypes { Op(1, "==", "Determines whether two values are equal", new[] { "equals" }), Op(2, "!=", "Determines whether two values are not equal", new[] { "not equals" }), - Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than" }), - Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than" }), - Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than" }), - Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than" }), + Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than", "more than" }), + Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than", "tinier than" }), + Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than", "tinier equals than" }), + Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than", "more equals than" }), new NodeArchetype { TypeID = 7, diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index ad88fa56a..19bb767d4 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new EnumComboBox(type) { EnumTypeValue = Values[0], - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, FlaxEditor.Surface.Constants.BoxRowHeight), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.EnumTypeValue); @@ -218,7 +218,7 @@ namespace FlaxEditor.Surface.Archetypes _output = (OutputBox)Elements[0]; _typePicker = new TypePickerControl { - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _typePicker.ValueChanged += () => Set(3); @@ -362,7 +362,7 @@ namespace FlaxEditor.Surface.Archetypes _output = (OutputBox)Elements[0]; _keyTypePicker = new TypePickerControl { - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _keyTypePicker.ValueChanged += OnKeyTypeChanged; @@ -482,7 +482,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + Size = new Float2(90, 20), DefaultValues = new object[] { false @@ -515,7 +515,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + Size = new Float2(120, 20), DefaultValues = new object[] { 0 @@ -543,7 +543,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + Size = new Float2(120, 20), DefaultValues = new object[] { 0.0f @@ -750,7 +750,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "PI", Description = "A value specifying the approximation of Ï€ which is 180 degrees", Flags = NodeFlags.AllGraphs, - Size = new Float2(50, 20), + Size = new Float2(45, 20), Elements = new[] { NodeElementArchetype.Factory.Output(0, "Ï€", typeof(float), 0), @@ -782,7 +782,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, - Size = new Float2(170, 20), + Size = new Float2(130, 20), DefaultValues = new object[] { 0u @@ -824,7 +824,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, - Size = new Float2(110, 20), + Size = new Float2(120, 20), DefaultValues = new object[] { 0.0d diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 79d3948bf..e3deef368 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -267,7 +267,7 @@ namespace FlaxEditor.Surface.Archetypes { _nameField = new Label { - Width = 140.0f, + Size = new Float2(140, FlaxEditor.Surface.Constants.BoxRowHeight), TextColorHighlighted = Style.Current.ForegroundGrey, HorizontalAlignment = TextAlignment.Near, Parent = this, @@ -386,8 +386,7 @@ namespace FlaxEditor.Surface.Archetypes _types = surface.FunctionTypes; _typePicker = new ComboBox { - Location = new Float2(4, 32), - Width = 80.0f, + Bounds = new Rectangle(4, 34, 80, FlaxEditor.Surface.Constants.BoxRowHeight), Parent = this, }; for (int i = 0; i < _types.Length; i++) @@ -455,8 +454,7 @@ namespace FlaxEditor.Surface.Archetypes _types = surface.FunctionTypes; _typePicker = new ComboBox { - Location = new Float2(24, 32), - Width = 80.0f, + Bounds = new Rectangle(24, 34, 80, FlaxEditor.Surface.Constants.BoxRowHeight), Parent = this, }; for (int i = 0; i < _types.Length; i++) @@ -631,7 +629,7 @@ namespace FlaxEditor.Surface.Archetypes } /// - protected override Color FooterColor => new Color(200, 11, 112); + protected override Color ArchetypeColor => new Color(200, 11, 112); /// public override void OnLoaded(SurfaceNodeActions action) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 56f594586..5b3aaf310 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -312,16 +312,17 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch) { _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array - Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size; + Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight), size; if (nodeArch.TypeID == 8) { - pos += new Float2(60, 0); - size = new Float2(172, 200); + pos += new Float2(65, 0); + size = new Float2(160, 185); + _sizeMin = new Float2(240, 185); } else { - pos += new Float2(0, 40); - size = new Float2(300, 200); + pos += new Float2(0, 40 + FlaxEditor.Utilities.Constants.UIMargin * 2f); + size = new Float2(300, 180); } _textBox = new CustomCodeTextBox { @@ -506,8 +507,8 @@ namespace FlaxEditor.Surface.Archetypes Size = new Float2(300, 200), DefaultValues = new object[] { - "// Here you can add HLSL code\nOutput0 = Input0;", - new Float2(300, 200), + "// You can add HLSL code here\nOutput0 = Input0;", + new Float2(350, 200), }, Elements = new[] { @@ -931,7 +932,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 36, - Title = "HSVToRGB", + Title = "HSV To RGB", Description = "Converts a HSV value to linear RGB [X = 0/360, Y = 0/1, Z = 0/1]", Flags = NodeFlags.MaterialGraph, Size = new Float2(160, 25), @@ -948,7 +949,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 37, - Title = "RGBToHSV", + Title = "RGB To HSV", Description = "Converts a linear RGB value to HSV [X = 0/360, Y = 0/1, Z = 0/1]", Flags = NodeFlags.MaterialGraph, Size = new Float2(160, 25), @@ -972,7 +973,7 @@ namespace FlaxEditor.Surface.Archetypes Size = new Float2(300, 240), DefaultValues = new object[] { - "// Here you can add HLSL code\nfloat4 GetCustomColor()\n{\n\treturn float4(1, 0, 0, 1);\n}", + "// You can add HLSL code here\nfloat4 GetCustomColor()\n{\n\treturn float4(1, 0, 0, 1);\n}", true, (int)MaterialTemplateInputsMapping.Utilities, new Float2(300, 240), diff --git a/Source/Editor/Surface/Archetypes/Math.cs b/Source/Editor/Surface/Archetypes/Math.cs index 26d271264..9d2ae3bfd 100644 --- a/Source/Editor/Surface/Archetypes/Math.cs +++ b/Source/Editor/Surface/Archetypes/Math.cs @@ -213,7 +213,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 30, Title = "Vector Transform", Description = "Transform vector from source space to destination space", - Flags = NodeFlags.MaterialGraph, + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, Size = new Float2(170, 40), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 905f793c9..61d3b3617 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -13,6 +13,7 @@ using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEngine; using FlaxEngine.Utilities; +using FlaxEditor.Surface.Undo; namespace FlaxEditor.Surface.Archetypes { @@ -92,7 +93,17 @@ namespace FlaxEditor.Surface.Archetypes var selected = GetSelected(); var selectedID = selected?.ID ?? Guid.Empty; if (selectedID != (Guid)Values[0]) + { + if (Surface.Undo != null && Surface.Undo.Enabled) + { + // Capture node connections to support undo + var action = new EditNodeConnections(Context, this); + RemoveConnections(); + action.End(); + Surface.AddBatchedUndoAction(action); + } Set(selected, ref selectedID); + } } /// diff --git a/Source/Editor/Surface/Archetypes/ParticleModules.cs b/Source/Editor/Surface/Archetypes/ParticleModules.cs index 4a68c31e0..e85c87898 100644 --- a/Source/Editor/Surface/Archetypes/ParticleModules.cs +++ b/Source/Editor/Surface/Archetypes/ParticleModules.cs @@ -121,7 +121,7 @@ namespace FlaxEditor.Surface.Archetypes Render2D.DrawRectangle(new Rectangle(1, 0, Width - 2, Height - 1), Colors[idx]); // Close button - Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); // Arrange button var dragBarColor = _arrangeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey; @@ -138,6 +138,12 @@ namespace FlaxEditor.Surface.Archetypes } } + /// + public override void Resize(float width, float height) + { + // Do nothing so module does not change size + } + private bool ArrangeAreaCheck(out int index, out Rectangle rect) { var barSidesExtend = 20.0f; @@ -261,9 +267,9 @@ namespace FlaxEditor.Surface.Archetypes const float closeButtonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; const float closeButtonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); - _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); + _closeButtonRect = new Rectangle(Width - closeButtonSize * 0.75f - closeButtonMargin, closeButtonMargin + 0.25f, closeButtonSize * 0.75f, closeButtonSize * 0.75f); _footerRect = Rectangle.Empty; - _enabled.Location = new Float2(_closeButtonRect.X - _enabled.Width - 2, _closeButtonRect.Y); + _enabled.Location = new Float2(_closeButtonRect.X - _enabled.Width - 2, _closeButtonRect.Y - 0.25f); _arrangeButtonRect = new Rectangle(_enabled.X - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); } @@ -461,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes /// /// The particle module node elements offset applied to controls to reduce default surface node header thickness. /// - private const float NodeElementsOffset = 16.0f - Surface.Constants.NodeHeaderSize; + private const float NodeElementsOffset = 16.0f - Surface.Constants.NodeHeaderHeight; private const NodeFlags DefaultModuleFlags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoMove; diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs index 59af7af5e..e0d7f2eb3 100644 --- a/Source/Editor/Surface/Archetypes/Particles.cs +++ b/Source/Editor/Surface/Archetypes/Particles.cs @@ -74,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes /// /// The header height. /// - public const float HeaderHeight = FlaxEditor.Surface.Constants.NodeHeaderSize; + public const float HeaderHeight = FlaxEditor.Surface.Constants.NodeHeaderHeight; /// /// Gets the type of the module. @@ -199,7 +199,7 @@ namespace FlaxEditor.Surface.Archetypes DrawChildren(); // Options border - var optionsAreaStart = FlaxEditor.Surface.Constants.NodeHeaderSize + 3.0f; + var optionsAreaStart = FlaxEditor.Surface.Constants.NodeHeaderHeight + 3.0f; var optionsAreaHeight = 7 * FlaxEditor.Surface.Constants.LayoutOffsetY + 6.0f; Render2D.DrawRectangle(new Rectangle(1, optionsAreaStart, Width - 2, optionsAreaHeight), style.BackgroundSelected); @@ -340,7 +340,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new ParticleEmitterNode(id, context, arch, groupArch), Title = "Particle Emitter", Description = "Main particle emitter node. Contains a set of modules per emitter context. Modules are executed in order from top to bottom of the stack.", - Flags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoRemove | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste | NodeFlags.NoCloseButton, + Flags = NodeFlags.ParticleEmitterGraph | NodeFlags.NoRemove | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste | NodeFlags.NoCloseButton | NodeFlags.FixedSize, Size = new Float2(300, 600), DefaultValues = new object[] { diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index f09ee5015..929cb153b 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -57,7 +57,7 @@ namespace FlaxEditor.Surface.Archetypes { _textureGroupPicker = new ComboBox { - Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level), + Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight + FlaxEditor.Surface.Constants.LayoutOffsetY * _level), Width = 100, Parent = this, }; @@ -133,8 +133,8 @@ namespace FlaxEditor.Surface.Archetypes Title = "Texture", Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", - Flags = NodeFlags.MaterialGraph, - Size = new Float2(140, 120), + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, + Size = new Float2(140, 140), DefaultValues = new object[] { Guid.Empty @@ -158,7 +158,7 @@ namespace FlaxEditor.Surface.Archetypes AlternativeTitles = new string[] { "UV", "UVs" }, Description = "Texture coordinates", Flags = NodeFlags.MaterialGraph, - Size = new Float2(150, 30), + Size = new Float2(160, 20), DefaultValues = new object[] { 0u @@ -176,8 +176,8 @@ namespace FlaxEditor.Surface.Archetypes Title = "Cube Texture", Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", - Flags = NodeFlags.MaterialGraph, - Size = new Float2(140, 120), + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, + Size = new Float2(140, 140), DefaultValues = new object[] { Guid.Empty @@ -239,7 +239,7 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(3, "Max Steps", true, typeof(float), 3, 2), NodeElementArchetype.Factory.Input(4, "Heightmap Texture", true, typeof(FlaxEngine.Object), 4), NodeElementArchetype.Factory.Output(0, "Parallax UVs", typeof(Float2), 5), - NodeElementArchetype.Factory.Text(Surface.Constants.BoxSize + 4, 5 * Surface.Constants.LayoutOffsetY, "Channel"), + NodeElementArchetype.Factory.Text(Surface.Constants.BoxRowHeight + 4, 5 * Surface.Constants.LayoutOffsetY, "Channel"), NodeElementArchetype.Factory.ComboBox(70, 5 * Surface.Constants.LayoutOffsetY, 50, 3, new[] { "R", @@ -493,7 +493,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Lightmap UV", - AlternativeTitles = new string[] { "Lightmap TexCoord" }, + AlternativeTitles = new string[] { "Lightmap TexCoord" }, Description = "Lightmap UVs", Flags = NodeFlags.MaterialGraph, Size = new Float2(110, 20), diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 60ecfaba6..24a0730df 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -467,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new CurveNode(id, context, arch, groupArch), Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.", Flags = NodeFlags.AllGraphs, - Size = new Float2(400, 180), + Size = new Float2(400, 180.0f), DefaultValues = new object[] { // Keyframes count @@ -485,7 +485,7 @@ namespace FlaxEditor.Surface.Archetypes zero, // Tangent In zero, // Tangent Out - // Empty keys zero-6 + // Empty keys 0-6 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, @@ -515,13 +515,12 @@ namespace FlaxEditor.Surface.Archetypes base.OnLoaded(action); // Create curve editor - var upperLeft = GetBox(0).BottomLeft; - var upperRight = GetBox(1).BottomRight; - float curveMargin = 20.0f; + var upperLeft = GetBox(0).BottomRight; + var upperRight = GetBox(1).BottomLeft; _curve = new BezierCurveEditor { MaxKeyframes = 7, - Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f), + Bounds = new Rectangle(upperLeft + new Float2(0f, 10.0f), upperRight.X - upperLeft.X - 8.0f, 135.0f), Parent = this, AnchorMax = Float2.One, }; @@ -841,7 +840,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = ScriptType.FlaxObject, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -910,7 +909,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = ScriptType.Object, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 140, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 140, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -961,7 +960,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = ScriptType.FlaxObject, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -1012,7 +1011,7 @@ namespace FlaxEditor.Surface.Archetypes _picker = new TypePickerControl { Type = type, - Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize, 160, 16), + Bounds = new Rectangle(FlaxEditor.Surface.Constants.NodeMarginX + 20, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderHeight, 160, 16), Parent = this, }; _picker.ValueChanged += () => SetValue(0, _picker.ValueTypeName); @@ -1071,7 +1070,11 @@ namespace FlaxEditor.Surface.Archetypes internal class RerouteNode : SurfaceNode, IConnectionInstigator { - internal static readonly Float2 DefaultSize = new Float2(FlaxEditor.Surface.Constants.BoxSize); + internal static readonly Float2 DefaultSize = new Float2(FlaxEditor.Surface.Constants.BoxRowHeight); + + internal bool DrawDisabled => _input.AllConnectionsDisabled || _output.AllConnectionsDisabled; + internal override bool DrawBasicShadow => false; + private Rectangle _localBounds; private InputBox _input; private OutputBox _output; @@ -1174,9 +1177,18 @@ namespace FlaxEditor.Surface.Archetypes _footerRect = Rectangle.Empty; } + /// + public override void Resize(float width, float height) + { + // Do nothing so the input and output boxes do not change position + } + /// public override void Draw() { + // Update active state of input + _input.IsActive = !_output.AllConnectionsDisabled; + var style = Surface.Style; var connectionColor = style.Colors.Default; var type = ScriptType.Null; @@ -1190,6 +1202,10 @@ namespace FlaxEditor.Surface.Archetypes Surface.Style.GetConnectionColor(type, hints, out connectionColor); } + // Draw the box as disabled if needed + if (DrawDisabled) + connectionColor = connectionColor * 0.6f; + if (!_input.HasAnyConnection) Render2D.FillRectangle(new Rectangle(-barHorizontalOffset - barHeight * 2, (DefaultSize.Y - barHeight) / 2, barHeight * 2, barHeight), connectionColor); if (!_output.HasAnyConnection) @@ -1200,6 +1216,11 @@ namespace FlaxEditor.Surface.Archetypes icon = type.IsVoid ? style.Icons.ArrowClose : style.Icons.BoxClose; else icon = type.IsVoid ? style.Icons.ArrowOpen : style.Icons.BoxOpen; + + // Shadow + var shadowRect = _localBounds.MakeOffsetted(ShadowOffset); + Render2D.DrawSprite(icon, shadowRect, Color.Black.AlphaMultiplied(0.125f)); + Render2D.DrawSprite(icon, _localBounds, connectionColor); base.Draw(); @@ -1444,8 +1465,8 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 6, Title = "Panner", Description = "Animates UVs over time", - Flags = NodeFlags.MaterialGraph, - Size = new Float2(170, 80), + Flags = NodeFlags.MaterialGraph | NodeFlags.FixedSize, + Size = new Float2(170, 96), DefaultValues = new object[] { false @@ -1455,7 +1476,7 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), NodeElementArchetype.Factory.Input(1, "Time", true, typeof(float), 1), NodeElementArchetype.Factory.Input(2, "Speed", true, typeof(Float2), 2), - NodeElementArchetype.Factory.Text(18, Surface.Constants.LayoutOffsetY * 3 + 5, "Fractional Part"), + NodeElementArchetype.Factory.Text(20, Surface.Constants.LayoutOffsetY * 3 + 5, "Fractional Part"), NodeElementArchetype.Factory.Bool(0, Surface.Constants.LayoutOffsetY * 3 + 5, 0), NodeElementArchetype.Factory.Output(0, "", typeof(Float2), 3) } @@ -1505,7 +1526,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Color Gradient", Create = (id, context, arch, groupArch) => new ColorGradientNode(id, context, arch, groupArch), Description = "Linear color gradient sampler", - Flags = NodeFlags.AllGraphs, + Flags = NodeFlags.AllGraphs | NodeFlags.FixedSize, Size = new Float2(400, 150.0f), DefaultValues = new object[] { @@ -1825,7 +1846,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Reroute", Create = (id, context, arch, groupArch) => new RerouteNode(id, context, arch, groupArch), Description = "Reroute a connection.", - Flags = NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaGUI | NodeFlags.AllGraphs, + Flags = NodeFlags.NoCloseButton | NodeFlags.NoSpawnViaGUI | NodeFlags.AllGraphs | NodeFlags.FixedSize, Size = RerouteNode.DefaultSize, ConnectionsHints = ConnectionsHint.All, IndependentBoxes = new int[] { 0 }, diff --git a/Source/Editor/Surface/Constants.cs b/Source/Editor/Surface/Constants.cs index 0f3dca783..eba53cc69 100644 --- a/Source/Editor/Surface/Constants.cs +++ b/Source/Editor/Surface/Constants.cs @@ -13,46 +13,61 @@ namespace FlaxEditor.Surface /// /// The node close button size. /// - public const float NodeCloseButtonSize = 12.0f; + public const float NodeCloseButtonSize = 10.0f; /// /// The node close button margin from the edges. /// - public const float NodeCloseButtonMargin = 2.0f; + public const float NodeCloseButtonMargin = 5.0f; /// /// The node header height. /// - public const float NodeHeaderSize = 28.0f; + public const float NodeHeaderHeight = 25.0f; + + /// + /// The scale of the header text. + /// + public const float NodeHeaderTextScale = 0.8f; /// /// The node footer height. /// - public const float NodeFooterSize = 4.0f; + public const float NodeFooterSize = 2.0f; /// - /// The node left margin. + /// The horizontal node margin. /// - public const float NodeMarginX = 5.0f; + public const float NodeMarginX = 6.0f; /// - /// The node right margin. + /// The vertical node right margin. /// - public const float NodeMarginY = 5.0f; + public const float NodeMarginY = 8.0f; /// - /// The box position offset on the x axis. + /// The size of the row that is started by a box. /// - public const float BoxOffsetX = 2.0f; + public const float BoxRowHeight = 19.0f; /// /// The box size (with and height). /// - public const float BoxSize = 20.0f; + public const float BoxSize = 15.0f; /// /// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent. /// - public const float LayoutOffsetY = 20.0f; + public const float LayoutOffsetY = 24.0f; + + /// + /// The offset between the box text and the box + /// + public const float BoxTextOffset = 1.65f; + + /// + /// The width of the rectangle used to draw the box text. + /// + public const float BoxTextRectWidth = 500.0f; } } diff --git a/Source/Editor/Surface/Elements/BoolValue.cs b/Source/Editor/Surface/Elements/BoolValue.cs index 70ded4dec..457e25f26 100644 --- a/Source/Editor/Surface/Elements/BoolValue.cs +++ b/Source/Editor/Surface/Elements/BoolValue.cs @@ -33,7 +33,7 @@ namespace FlaxEditor.Surface.Elements /// public BoolValue(SurfaceNode parentNode, NodeElementArchetype archetype) - : base(parentNode, archetype, archetype.ActualPosition, new Float2(16), true) + : base(parentNode, archetype, archetype.ActualPosition, new Float2(Constants.BoxRowHeight), true) { } diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index 964b3cc69..a110192cf 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FlaxEditor.Scripting; using FlaxEditor.Surface.Undo; using FlaxEngine; @@ -194,9 +195,19 @@ namespace FlaxEditor.Surface.Elements set => _isActive = value; } + /// + /// Gets if the box is disabled (user can still connect, but connections will be ignored). + /// + public bool IsDisabled => !(Enabled && IsActive); + + /// + /// Gets a value indicating whether all connections are disabled. + /// + public bool AllConnectionsDisabled => Connections.All(c => c.IsDisabled); + /// protected Box(SurfaceNode parentNode, NodeElementArchetype archetype, Float2 location) - : base(parentNode, archetype, location, new Float2(Constants.BoxSize), false) + : base(parentNode, archetype, location, new Float2(Constants.BoxRowHeight), false) { _currentType = DefaultType; _isSingle = Archetype.Single; @@ -352,10 +363,10 @@ namespace FlaxEditor.Surface.Elements Assert.AreEqual(r1, r2); // Update - ConnectionTick(); - box.ConnectionTick(); OnConnectionsChanged(); box.OnConnectionsChanged(); + ConnectionTick(); + box.ConnectionTick(); Surface?.OnNodesDisconnected(this, box); } @@ -379,10 +390,10 @@ namespace FlaxEditor.Surface.Elements Assert.IsTrue(AreConnected(box)); // Update - ConnectionTick(); - box.ConnectionTick(); OnConnectionsChanged(); box.OnConnectionsChanged(); + ConnectionTick(); + box.ConnectionTick(); Surface?.OnNodesConnected(this, box); } diff --git a/Source/Editor/Surface/Elements/ColorValue.cs b/Source/Editor/Surface/Elements/ColorValue.cs index 671eb74f8..45af92273 100644 --- a/Source/Editor/Surface/Elements/ColorValue.cs +++ b/Source/Editor/Surface/Elements/ColorValue.cs @@ -28,6 +28,7 @@ namespace FlaxEditor.Surface.Elements public ColorValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; UseDynamicEditing = false; diff --git a/Source/Editor/Surface/Elements/ComboBoxElement.cs b/Source/Editor/Surface/Elements/ComboBoxElement.cs index cb1ed79a5..0f06274a4 100644 --- a/Source/Editor/Surface/Elements/ComboBoxElement.cs +++ b/Source/Editor/Surface/Elements/ComboBoxElement.cs @@ -33,6 +33,7 @@ namespace FlaxEditor.Surface.Elements public ComboBoxElement(SurfaceNode parentNode, NodeElementArchetype archetype) : base(archetype.ActualPositionX, archetype.ActualPositionY, archetype.Size.X) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/Elements/EnumValue.cs b/Source/Editor/Surface/Elements/EnumValue.cs index 9aceaeff4..1fe87fef0 100644 --- a/Source/Editor/Surface/Elements/EnumValue.cs +++ b/Source/Editor/Surface/Elements/EnumValue.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI; +using FlaxEngine; using FlaxEngine.Utilities; namespace FlaxEditor.Surface.Elements @@ -29,9 +30,7 @@ namespace FlaxEditor.Surface.Elements public EnumValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(TypeUtils.GetType(archetype.Text).Type) { - X = archetype.ActualPositionX; - Y = archetype.ActualPositionY; - Width = archetype.Size.X; + Bounds = new Rectangle(archetype.ActualPositionX, archetype.ActualPositionY, archetype.Size.X, Constants.BoxRowHeight); ParentNode = parentNode; Archetype = archetype; Value = Convert.ToInt32(ParentNode.Values[Archetype.ValueIndex]); diff --git a/Source/Editor/Surface/Elements/FloatValue.cs b/Source/Editor/Surface/Elements/FloatValue.cs index a90ebb8b4..393145169 100644 --- a/Source/Editor/Surface/Elements/FloatValue.cs +++ b/Source/Editor/Surface/Elements/FloatValue.cs @@ -29,6 +29,7 @@ namespace FlaxEditor.Surface.Elements public FloatValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y, 50, archetype.ValueMin, archetype.ValueMax, 0.01f) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/Elements/InputBox.cs b/Source/Editor/Surface/Elements/InputBox.cs index 9861ebacd..15548b9d9 100644 --- a/Source/Editor/Surface/Elements/InputBox.cs +++ b/Source/Editor/Surface/Elements/InputBox.cs @@ -120,7 +120,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = IntegerValue.Get(box.ParentNode, box.Archetype, box.Value); - var width = 40; + var width = 50; var control = new IntValueBox(value, bounds.X, bounds.Y, width + 12, int.MinValue, int.MaxValue, 0.01f) { Height = bounds.Height, @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = UnsignedIntegerValue.Get(box.ParentNode, box.Archetype, box.Value); - var width = 40; + var width = 50; var control = new UIntValueBox(value, bounds.X, bounds.Y, width + 12, uint.MinValue, uint.MaxValue, 0.01f) { Height = bounds.Height, @@ -212,7 +212,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = FloatValue.Get(box.ParentNode, box.Archetype, box.Value); - var width = 40; + var width = 50; var control = new FloatValueBox(value, bounds.X, bounds.Y, width + 12, float.MinValue, float.MaxValue, 0.01f) { Height = bounds.Height, @@ -303,7 +303,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 2 - 2, bounds.Height) { ClipChildren = false, @@ -377,7 +377,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -460,7 +460,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 4 - 2, bounds.Height) { ClipChildren = false, @@ -553,7 +553,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 2 - 2, bounds.Height) { ClipChildren = false, @@ -627,7 +627,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -710,7 +710,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 4 - 2, bounds.Height) { ClipChildren = false, @@ -803,7 +803,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 2 - 2, bounds.Height) { ClipChildren = false, @@ -877,7 +877,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 30; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -960,7 +960,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box); - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 4 - 2, bounds.Height) { ClipChildren = false, @@ -1053,7 +1053,7 @@ namespace FlaxEditor.Surface.Elements public Control Create(InputBox box, ref Rectangle bounds) { var value = GetValue(box).EulerAngles; - var width = 20; + var width = 50; var control = new ContainerControl(bounds.X, bounds.Y, (width + 2) * 3 - 2, bounds.Height) { ClipChildren = false, @@ -1442,8 +1442,8 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; - var rect = new Rectangle(Width + 4, 0, 1410, Height); - Render2D.DrawText(style.FontSmall, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); + var rect = new Rectangle(Width + Constants.BoxTextOffset, 0, Constants.BoxTextRectWidth, Height); + Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); } /// diff --git a/Source/Editor/Surface/Elements/IntegerValue.cs b/Source/Editor/Surface/Elements/IntegerValue.cs index dde284211..77f4297ca 100644 --- a/Source/Editor/Surface/Elements/IntegerValue.cs +++ b/Source/Editor/Surface/Elements/IntegerValue.cs @@ -32,6 +32,7 @@ namespace FlaxEditor.Surface.Elements public IntegerValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y, 50, (int)archetype.ValueMin, (int)archetype.ValueMax, 0.05f) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index 8836dc0dc..e1ec56a36 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -186,7 +186,7 @@ namespace FlaxEditor.Surface.Elements Box targetBox = Connections[i]; var endPos = targetBox.ConnectionOrigin; var highlight = DefaultConnectionThickness + Mathf.Max(startHighlight, targetBox.ConnectionsHighlightIntensity); - var alpha = targetBox.Enabled && targetBox.IsActive ? 1.0f : 0.6f; + var alpha = targetBox.IsDisabled ? 0.6f : 1.0f; // We have to calculate an offset here to preserve the original color for when the default connection thickness is larger than 1 var highlightOffset = (highlight - (DefaultConnectionThickness - 1)); @@ -212,7 +212,7 @@ namespace FlaxEditor.Surface.Elements // Draw all the connections var startPos = ConnectionOrigin; var endPos = targetBox.ConnectionOrigin; - var alpha = targetBox.Enabled && targetBox.IsActive ? 1.0f : 0.6f; + var alpha = targetBox.IsDisabled ? 0.6f : 1.0f; var color = _currentTypeColor * alpha; DrawConnection(Surface.Style, ref startPos, ref endPos, ref color, SelectedConnectionThickness); } @@ -230,8 +230,8 @@ namespace FlaxEditor.Surface.Elements // Draw text var style = Style.Current; - var rect = new Rectangle(-100, 0, 100 - 2, Height); - Render2D.DrawText(style.FontSmall, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center); + var rect = new Rectangle(-Constants.BoxTextRectWidth - Constants.BoxTextOffset * 2f, 0f, Constants.BoxTextRectWidth, Height); + Render2D.DrawText(style.FontMedium, Text, rect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Far, TextAlignment.Center); } } } diff --git a/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs b/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs index b05a27286..621629819 100644 --- a/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs +++ b/Source/Editor/Surface/Elements/UnsignedIntegerValue.cs @@ -24,6 +24,7 @@ namespace FlaxEditor.Surface.Elements public UnsignedIntegerValue(SurfaceNode parentNode, NodeElementArchetype archetype) : base(Get(parentNode, archetype), archetype.Position.X, archetype.Position.Y, 50, (uint)archetype.ValueMin, (uint)archetype.ValueMax, 0.05f) { + Height = Constants.BoxRowHeight; ParentNode = parentNode; Archetype = archetype; diff --git a/Source/Editor/Surface/NodeElementArchetype.cs b/Source/Editor/Surface/NodeElementArchetype.cs index f5a23d411..6db191347 100644 --- a/Source/Editor/Surface/NodeElementArchetype.cs +++ b/Source/Editor/Surface/NodeElementArchetype.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; using System.Reflection; -using FlaxEditor.CustomEditors; +using FlaxEditor.GUI.Input; using FlaxEditor.Scripting; +using FlaxEditor.Surface.Elements; using FlaxEngine; namespace FlaxEditor.Surface @@ -78,12 +79,12 @@ namespace FlaxEditor.Surface /// /// Gets the actual element position on the y axis. /// - public float ActualPositionY => Position.Y + Constants.NodeMarginY + Constants.NodeHeaderSize; + public float ActualPositionY => Position.Y + Constants.NodeMarginY + Constants.NodeHeaderHeight; /// /// Gets the actual element position. /// - public Float2 ActualPosition => new Float2(Position.X + Constants.NodeMarginX, Position.Y + Constants.NodeMarginY + Constants.NodeHeaderSize); + public Float2 ActualPosition => new Float2(Position.X + Constants.NodeMarginX, Position.Y + Constants.NodeMarginY + Constants.NodeHeaderHeight); /// /// Node element archetypes factory object. Helps to build surface nodes archetypes. @@ -106,8 +107,8 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Input, Position = new Float2( - Constants.NodeMarginX - Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + Constants.NodeMarginX, + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = valueIndex, @@ -132,8 +133,8 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Input, Position = new Float2( - Constants.NodeMarginX - Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + Constants.NodeMarginX, + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = valueIndex, @@ -157,8 +158,8 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxSize + Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + -Constants.NodeMarginX, + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = -1, @@ -182,8 +183,8 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Output, Position = new Float2( - Constants.NodeMarginX - Constants.BoxSize + Constants.BoxOffsetX, - Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY), + -Constants.NodeMarginX, + Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY), Text = text, Single = single, ValueIndex = -1, @@ -205,6 +206,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.BoolValue, Position = new Float2(x, y), + Size = new Float2(16f), Text = null, Single = false, ValueIndex = valueIndex, @@ -228,7 +230,8 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.IntegerValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(50f, IntegerValue.DefaultHeight), Text = null, Single = false, ValueIndex = valueIndex, @@ -254,7 +257,8 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.UnsignedIntegerValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(50f, UnsignedIntegerValue.DefaultHeight), Text = null, Single = false, ValueIndex = valueIndex, @@ -280,7 +284,8 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.FloatValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(50f, FloatValueBox.DefaultHeight), Text = null, Single = false, ValueIndex = valueIndex, @@ -359,7 +364,8 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.ColorValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), + Size = new Float2(32, 18), Text = null, Single = false, ValueIndex = valueIndex, @@ -382,6 +388,7 @@ namespace FlaxEditor.Surface { Type = NodeElementType.Asset, Position = new Float2(x, y), + Size = new Float2(78f, 90f), Text = type.FullName, Single = false, ValueIndex = valueIndex, @@ -499,7 +506,7 @@ namespace FlaxEditor.Surface /// The control height. /// The control tooltip text. /// The archetype. - public static NodeElementArchetype Text(float x, float y, string text, float width = 100.0f, float height = 16.0f, string tooltip = null) + public static NodeElementArchetype Text(float x, float y, string text, float width = 100.0f, float height = 19.0f, string tooltip = null) { return new NodeElementArchetype { @@ -530,7 +537,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.TextBox, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Size = new Float2(width, height), Single = false, ValueIndex = valueIndex, @@ -595,7 +602,7 @@ namespace FlaxEditor.Surface return new NodeElementArchetype { Type = NodeElementType.BoxValue, - Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderSize + y), + Position = new Float2(Constants.NodeMarginX + x, Constants.NodeMarginY + Constants.NodeHeaderHeight + y), Text = null, Single = false, ValueIndex = valueIndex, diff --git a/Source/Editor/Surface/NodeFactory.cs b/Source/Editor/Surface/NodeFactory.cs index 7c85cb449..f787899dc 100644 --- a/Source/Editor/Surface/NodeFactory.cs +++ b/Source/Editor/Surface/NodeFactory.cs @@ -60,84 +60,84 @@ namespace FlaxEditor.Surface { GroupID = 1, Name = "Material", - Color = new Color(231, 76, 60), + Color = new Color(181, 89, 49), Archetypes = Archetypes.Material.Nodes }, new GroupArchetype { GroupID = 2, Name = "Constants", - Color = new Color(243, 156, 18), + Color = new Color(163, 106, 21), Archetypes = Archetypes.Constants.Nodes }, new GroupArchetype { GroupID = 3, Name = "Math", - Color = new Color(52, 152, 219), + Color = new Color(45, 126, 181), Archetypes = Archetypes.Math.Nodes }, new GroupArchetype { GroupID = 4, Name = "Packing", - Color = new Color(155, 89, 182), + Color = new Color(124, 66, 143), Archetypes = Archetypes.Packing.Nodes }, new GroupArchetype { GroupID = 5, Name = "Textures", - Color = new Color(46, 204, 113), + Color = new Color(43, 130, 83), Archetypes = Archetypes.Textures.Nodes }, new GroupArchetype { GroupID = 6, Name = "Parameters", - Color = new Color(52, 73, 94), + Color = new Color(55, 78, 99), Archetypes = Archetypes.Parameters.Nodes }, new GroupArchetype { GroupID = 7, Name = "Tools", - Color = new Color(149, 165, 166), + Color = new Color(88, 96, 97), Archetypes = Archetypes.Tools.Nodes }, new GroupArchetype { GroupID = 8, Name = "Layers", - Color = new Color(249, 105, 116), + Color = new Color(189, 75, 81), Archetypes = Archetypes.Layers.Nodes }, new GroupArchetype { GroupID = 9, Name = "Animations", - Color = new Color(105, 179, 160), + Color = new Color(72, 125, 107), Archetypes = Archetypes.Animation.Nodes }, new GroupArchetype { GroupID = 10, Name = "Boolean", - Color = new Color(237, 28, 36), + Color = new Color(166, 27, 32), Archetypes = Archetypes.Boolean.Nodes }, new GroupArchetype { GroupID = 11, Name = "Bitwise", - Color = new Color(181, 230, 29), + Color = new Color(96, 125, 34), Archetypes = Archetypes.Bitwise.Nodes }, new GroupArchetype { GroupID = 12, Name = "Comparisons", - Color = new Color(148, 30, 34), + Color = new Color(166, 33, 57), Archetypes = Archetypes.Comparisons.Nodes }, // GroupID = 13 -> Custom Nodes provided externally @@ -145,7 +145,7 @@ namespace FlaxEditor.Surface { GroupID = 14, Name = "Particles", - Color = new Color(121, 210, 176), + Color = new Color(72, 125, 107), Archetypes = Archetypes.Particles.Nodes }, new GroupArchetype @@ -166,7 +166,7 @@ namespace FlaxEditor.Surface { GroupID = 17, Name = "Flow", - Color = new Color(237, 136, 64), + Color = new Color(181, 91, 33), Archetypes = Archetypes.Flow.Nodes }, new GroupArchetype diff --git a/Source/Editor/Surface/NodeFlags.cs b/Source/Editor/Surface/NodeFlags.cs index 27900bdb3..4201d9da1 100644 --- a/Source/Editor/Surface/NodeFlags.cs +++ b/Source/Editor/Surface/NodeFlags.cs @@ -78,6 +78,11 @@ namespace FlaxEditor.Surface /// VariableValuesSize = 2048, + /// + /// Node has fixed size defined and should not use automatic layout. + /// + FixedSize = 4096, + /// /// Node can be used in the all visual graphs. /// diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs index f332471fb..c61d4365d 100644 --- a/Source/Editor/Surface/ParticleEmitterSurface.cs +++ b/Source/Editor/Surface/ParticleEmitterSurface.cs @@ -59,7 +59,7 @@ namespace FlaxEditor.Surface var width = _rootNode.Width; var rootPos = _rootNode.Location; var pos = rootPos; - pos.Y += Constants.NodeHeaderSize + 1.0f + 7 * Constants.LayoutOffsetY + 6.0f + 4.0f; + pos.Y += Constants.NodeHeaderHeight + 1.0f + 7 * Constants.LayoutOffsetY + 6.0f + 4.0f; for (int i = 0; i < _rootNode.Headers.Length; i++) { @@ -67,7 +67,7 @@ namespace FlaxEditor.Surface var modulesStart = pos - rootPos; var modules = modulesGroups.FirstOrDefault(x => x.Key == header.ModuleType); - pos.Y += Constants.NodeHeaderSize + 2.0f; + pos.Y += Constants.NodeHeaderHeight + 2.0f; if (modules != null) { foreach (var module in modules) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index 259c29836..ee6560a84 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -74,6 +74,12 @@ namespace FlaxEditor.Surface Resize(size.X, size.Y); } + /// + public override void ResizeAuto() + { + // Do nothing, we want to put full control of node size into the users hands + } + /// public override void Draw() { diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index a76fa245d..79a285bd8 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -56,10 +56,10 @@ namespace FlaxEditor.Surface : base(id, context, nodeArch, groupArch) { _sizeValueIndex = 2; // Index of the Size stored in Values array - _sizeMin = new Float2(140.0f, Constants.NodeHeaderSize); + _sizeMin = new Float2(140.0f, Constants.NodeHeaderHeight); _renameTextBox = new TextBox(false, 0, 0, Width) { - Height = Constants.NodeHeaderSize, + Height = Constants.NodeHeaderHeight, Visible = false, Parent = this, EndEditOnClick = false, // We have to handle this ourselves, otherwise the textbox instantly loses focus when double-clicking the header @@ -124,11 +124,11 @@ namespace FlaxEditor.Surface { base.UpdateRectangles(); - const float headerSize = Constants.NodeHeaderSize; + const float headerSize = Constants.NodeHeaderHeight; const float buttonMargin = Constants.NodeCloseButtonMargin; const float buttonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); - _closeButtonRect = new Rectangle(Width - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); + _closeButtonRect = new Rectangle(Width - buttonSize * 0.75f - buttonMargin, buttonMargin, buttonSize * 0.75f, buttonSize * 0.75f); _colorButtonRect = new Rectangle(_closeButtonRect.Left - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin, buttonSize, buttonSize); _renameTextBox.Width = Width; @@ -183,7 +183,7 @@ namespace FlaxEditor.Surface if (Surface.CanEdit) { // Close button - Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); // Color button Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 6312bd68d..d28d04e1e 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -40,6 +40,13 @@ namespace FlaxEditor.Surface [HideInEditor] public class SurfaceNode : SurfaceControl { + internal const float ShadowOffset = 2.25f; + + /// + /// If true, draws a basic rectangle shadow behind the node. Disable to hide shadow or if the node is drawing a custom shadow. + /// + internal virtual bool DrawBasicShadow => true; + /// /// The box to draw a highlight around. Drawing will be skipped if null. /// @@ -55,6 +62,11 @@ namespace FlaxEditor.Surface /// protected Rectangle _headerRect; + /// + /// The header text rectangle (local space). + /// + protected Rectangle _headerTextRect; + /// /// The close button rectangle (local space). /// @@ -123,7 +135,7 @@ namespace FlaxEditor.Surface /// The node archetype. /// The group archetype. public SurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) - : base(context, nodeArch.Size.X + Constants.NodeMarginX * 2, nodeArch.Size.Y + Constants.NodeMarginY * 2 + Constants.NodeHeaderSize + Constants.NodeFooterSize) + : base(context, nodeArch.Size.X + Constants.NodeMarginX * 2, nodeArch.Size.Y + Constants.NodeMarginY * 2 + Constants.NodeHeaderHeight + Constants.NodeFooterSize) { Title = nodeArch.Title; ID = id; @@ -132,7 +144,7 @@ namespace FlaxEditor.Surface AutoFocus = false; TooltipText = GetTooltip(); CullChildren = false; - BackgroundColor = Style.Current.BackgroundNormal; + BackgroundColor = Color.Lerp(Style.Current.Background, Style.Current.BackgroundHighlighted, 0.55f); if (Archetype.DefaultValues != null) { @@ -147,9 +159,9 @@ namespace FlaxEditor.Surface public virtual string ContentSearchText => null; /// - /// Gets the color of the footer of the node. + /// Gets the color of the header of the node. /// - protected virtual Color FooterColor => GroupArchetype.Color; + protected virtual Color ArchetypeColor => GroupArchetype.Color; private Float2 mouseDownMousePosition; @@ -161,7 +173,7 @@ namespace FlaxEditor.Surface /// The node control total size. protected virtual Float2 CalculateNodeSize(float width, float height) { - return new Float2(width + Constants.NodeMarginX * 2, height + Constants.NodeMarginY * 2 + Constants.NodeHeaderSize + Constants.NodeFooterSize); + return new Float2(width + Constants.NodeMarginX * 2, height + Constants.NodeMarginY * 2 + Constants.NodeHeaderHeight + Constants.NodeFooterSize); } /// @@ -169,7 +181,7 @@ namespace FlaxEditor.Surface /// /// The width. /// The height. - public void Resize(float width, float height) + public virtual void Resize(float width, float height) { if (Surface == null) return; @@ -187,7 +199,7 @@ namespace FlaxEditor.Surface { if (Elements[i] is OutputBox box) { - box.Location = box.Archetype.Position + new Float2(width, 0); + box.Location = box.Archetype.Position + new Float2(width - Constants.NodeMarginX, 0); } } @@ -215,30 +227,41 @@ namespace FlaxEditor.Surface var child = Children[i]; if (!child.Visible) continue; + + // Input boxes if (child is InputBox inputBox) { - var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 20; - if (inputBox.DefaultValueEditor != null) + var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 25; + if (inputBox.DefaultValueEditor != null && inputBox.DefaultValueEditor.Visible) boxWidth += inputBox.DefaultValueEditor.Width + 4; leftWidth = Mathf.Max(leftWidth, boxWidth); - leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f); + leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + Constants.BoxRowHeight); } + // Output boxes else if (child is OutputBox outputBox) { - rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 20); - rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f); + rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 25); + rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderHeight + Constants.BoxRowHeight); } + // Elements (Float-, int-, uint- value boxes, asset pickers, etc.) + // These will only ever be on the left side of the node, so we only adjust left width and height + else if (child is SurfaceNodeElementControl elementControl) + { + leftWidth = Mathf.Max(leftWidth, elementControl.Width + 8f); + leftHeight = Mathf.Max(leftHeight, elementControl.Height); + } + // Other controls in the node else if (child is Control control) { if (control.AnchorPreset == AnchorPresets.TopLeft) { - width = Mathf.Max(width, control.Right + 4 - Constants.NodeMarginX); - height = Mathf.Max(height, control.Bottom + 4 - Constants.NodeMarginY - Constants.NodeHeaderSize); + width = Mathf.Max(width, control.Right + 15 + Constants.NodeMarginX); + height = Mathf.Max(height, control.Bottom - Constants.NodeMarginY - Constants.NodeHeaderHeight); } else if (!_headerRect.Intersects(control.Bounds)) { - width = Mathf.Max(width, control.Width + 4); - height = Mathf.Max(height, control.Height + 4); + width = Mathf.Max(width, control.Width + 15 + Constants.NodeMarginX); + height = Mathf.Max(height, control.Height); } } } @@ -325,6 +348,9 @@ namespace FlaxEditor.Surface Elements.Add(element); if (element is Control control) AddChild(control); + + if (!IsLayoutLocked) + UpdateSize(); } /// @@ -365,7 +391,7 @@ namespace FlaxEditor.Surface // Sync properties for exiting box box.Text = text; box.CurrentType = type; - box.Y = Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY; + box.Y = Constants.NodeMarginY + Constants.NodeHeaderHeight + yLevel * Constants.LayoutOffsetY; } // Update box @@ -434,7 +460,7 @@ namespace FlaxEditor.Surface private static readonly List UpdateStack = new List(); /// - /// Updates dependant/independent boxes types. + /// Updates dependent/independent boxes types. /// public void UpdateBoxesTypes() { @@ -776,6 +802,24 @@ namespace FlaxEditor.Surface return output; } + /// + /// Draws the close button inside of the . + /// + /// The rectangle to draw the close button in. + /// The color of the close button. + public void DrawCloseButton(Rectangle rect, Color color) + { + // Disable vertex snapping to reduce artefacts at the line ends + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + + rect.Expand(-2f); // Don't overshoot the rectangle because of the thickness + Render2D.DrawLine(rect.TopLeft, rect.BottomRight, color, 2f); + Render2D.DrawLine(rect.BottomLeft, rect.TopRight, color, 2f); + + Render2D.Features = features; + } + /// /// Draws all the connections between surface objects related to this node. /// @@ -867,6 +911,14 @@ namespace FlaxEditor.Surface return sb.ToString(); } + private void UpdateSize() + { + if (Archetype.Flags.HasFlag(NodeFlags.FixedSize)) + Resize(Archetype.Size.X, Archetype.Size.Y); + else + ResizeAuto(); + } + /// protected override bool ShowTooltip => base.ShowTooltip && _headerRect.Contains(ref _mousePosition) && !Surface.IsLeftMouseButtonDown && !Surface.IsRightMouseButtonDown && !Surface.IsPrimaryMenuOpened; @@ -919,6 +971,8 @@ namespace FlaxEditor.Surface if (Elements[i] is Box box) box.OnConnectionsChanged(); } + + UpdateSize(); } /// @@ -956,6 +1010,8 @@ namespace FlaxEditor.Surface Surface.AddBatchedUndoAction(new EditNodeValuesAction(this, before, graphEdited)); _isDuringValuesEditing = false; + + UpdateSize(); } /// @@ -990,6 +1046,8 @@ namespace FlaxEditor.Surface } _isDuringValuesEditing = false; + + UpdateSize(); } internal void SetIsDuringValuesEditing(bool value) @@ -998,7 +1056,7 @@ namespace FlaxEditor.Surface } /// - /// Sets teh node values from the given pasted source. Can be overriden to perform validation or custom values processing. + /// Sets teh node values from the given pasted source. Can be overridden to perform validation or custom values processing. /// /// The input values array. public virtual void SetValuesPaste(object[] values) @@ -1022,16 +1080,18 @@ namespace FlaxEditor.Surface public virtual void ConnectionTick(Box box) { UpdateBoxesTypes(); + UpdateSize(); } /// protected override void UpdateRectangles() { const float footerSize = Constants.NodeFooterSize; - const float headerSize = Constants.NodeHeaderSize; + const float headerSize = Constants.NodeHeaderHeight; const float closeButtonMargin = Constants.NodeCloseButtonMargin; const float closeButtonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); + _headerTextRect = _headerRect with { X = 5f, Width = Width - closeButtonSize - closeButtonMargin * 4f }; _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); _footerRect = new Rectangle(0, Height - footerSize, Width, footerSize); } @@ -1040,9 +1100,16 @@ namespace FlaxEditor.Surface public override void Draw() { var style = Style.Current; + var backgroundRect = new Rectangle(Float2.Zero, Size); + + // Shadow + if (DrawBasicShadow) + { + var shadowRect = backgroundRect.MakeOffsetted(ShadowOffset); + Render2D.FillRectangle(shadowRect, Color.Black.AlphaMultiplied(0.125f)); + } // Background - var backgroundRect = new Rectangle(Float2.Zero, Size); Render2D.FillRectangle(backgroundRect, BackgroundColor); // Breakpoint hit @@ -1058,18 +1125,18 @@ namespace FlaxEditor.Surface var headerColor = style.BackgroundHighlighted; if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting) headerColor *= 1.07f; - Render2D.FillRectangle(_headerRect, headerColor); - Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center); + Render2D.FillRectangle(_headerRect, ArchetypeColor); + Render2D.DrawText(style.FontLarge, Title, _headerTextRect, style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, Constants.NodeHeaderTextScale); // Close button if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) { bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting; - Render2D.DrawSprite(style.Cross, _closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); + DrawCloseButton(_closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); } // Footer - Render2D.FillRectangle(_footerRect, FooterColor); + Render2D.FillRectangle(_footerRect, ArchetypeColor); DrawChildren(); @@ -1078,7 +1145,7 @@ namespace FlaxEditor.Surface { var colorTop = Color.Orange; var colorBottom = Color.OrangeRed; - Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom); + Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom, 2.5f); } // Breakpoint dot diff --git a/Source/Editor/Surface/SurfaceStyle.cs b/Source/Editor/Surface/SurfaceStyle.cs index be09f51be..2997e1d21 100644 --- a/Source/Editor/Surface/SurfaceStyle.cs +++ b/Source/Editor/Surface/SurfaceStyle.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.Scripting; +using FlaxEditor.Surface.Elements; using FlaxEngine; using FlaxEngine.Utilities; @@ -140,6 +141,11 @@ namespace FlaxEditor.Surface /// public Texture Background; + /// + /// The color used as a surface background. + /// + public Color BackgroundColor; + /// /// Boxes drawing callback. /// @@ -216,19 +222,20 @@ namespace FlaxEditor.Surface private static void DefaultDrawBox(Elements.Box box) { - var rect = new Rectangle(Float2.Zero, box.Size); + var rect = new Rectangle(box.Width * 0.5f - Constants.BoxSize * 0.5f, box.Height * 0.5f - Constants.BoxSize * 0.5f, new Float2(Constants.BoxSize)); // Size culling const float minBoxSize = 5.0f; if (rect.Size.LengthSquared < minBoxSize * minBoxSize) return; - // Debugging boxes size + // Debugging boxes size and bounds //Render2D.DrawRectangle(rect, Color.Orange); return; + //Render2D.DrawRectangle(box.Bounds, Color.Green); // Draw icon bool hasConnections = box.HasAnyConnection; - float alpha = box.Enabled && box.IsActive ? 1.0f : 0.6f; + float alpha = box.IsDisabled ? 0.6f : 1.0f; Color color = box.CurrentTypeColor * alpha; var style = box.Surface.Style; SpriteHandle icon; @@ -237,20 +244,42 @@ namespace FlaxEditor.Surface else icon = hasConnections ? style.Icons.BoxClose : style.Icons.BoxOpen; color *= box.ConnectionsHighlightIntensity + 1; + if (box.IsMouseOver) + { + color *= 1.3f; + rect = rect.MakeExpanded(1.0f); + } + + // Disable vertex snapping to prevent position jitter/snapping artefacts for the boxes when zooming the surface + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + Render2D.DrawSprite(icon, rect, color); + // Draw connected hint with color from connected output box + if (hasConnections && box.Connections[0] is OutputBox connectedOutputBox) + { + bool connectedSameColor = connectedOutputBox.CurrentTypeColor == box.CurrentTypeColor; + Color innerColor = connectedSameColor ? color.RGBMultiplied(0.4f) : connectedOutputBox.CurrentTypeColor; + innerColor = innerColor * alpha; + Render2D.DrawSprite(icon, rect.MakeExpanded(-5.0f), innerColor); + } + // Draw selection hint if (box.IsSelected) { float outlineAlpha = Mathf.Sin(Time.TimeSinceStartup * 4.0f) * 0.5f + 0.5f; float outlineWidth = Mathf.Lerp(1.5f, 4.0f, outlineAlpha); var outlineRect = new Rectangle(rect.X - outlineWidth, rect.Y - outlineWidth, rect.Width + outlineWidth * 2, rect.Height + outlineWidth * 2); - Render2D.DrawSprite(icon, outlineRect, FlaxEngine.GUI.Style.Current.BorderSelected.RGBMultiplied(1.0f + outlineAlpha * 0.4f)); + Color selectionColor = FlaxEngine.GUI.Style.Current.BorderSelected.RGBMultiplied(1.0f + outlineAlpha * 0.4f); + Render2D.DrawSprite(icon, outlineRect, selectionColor.AlphaMultiplied(0.4f)); } + + Render2D.Features = features; } /// - /// Function used to create style for the given surface type. Can be overriden to provide some customization via user plugin. + /// Function used to create style for the given surface type. Can be overridden to provide some customization via user plugin. /// public static Func CreateStyleHandler = CreateDefault; @@ -293,6 +322,7 @@ namespace FlaxEditor.Surface ArrowClose = editor.Icons.VisjectArrowClosed32, }, Background = editor.UI.VisjectSurfaceBackground, + BackgroundColor = new Color(31, 31, 31), }; } @@ -311,13 +341,11 @@ namespace FlaxEditor.Surface { var dir = sub / length; var arrowRect = new Rectangle(0, 0, 16.0f, 16.0f); - float rotation = Float2.Dot(dir, Float2.UnitY); - if (endPos.X < startPos.X) - rotation = 2 - rotation; + float rotation = Mathf.Atan2(dir.Y, dir.X); var sprite = Editor.Instance.Icons.VisjectArrowClosed32; var arrowTransform = Matrix3x3.Translation2D(-6.5f, -8) * - Matrix3x3.RotationZ(rotation * Mathf.PiOverTwo) * + Matrix3x3.RotationZ(rotation) * Matrix3x3.Translation2D(endPos - dir * 8); Render2D.PushTransform(ref arrowTransform); diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index a7d390132..5f5078d5f 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -65,31 +65,68 @@ namespace FlaxEditor.Surface /// protected virtual void DrawBackground() { - DrawBackgroundDefault(Style.Background, Width, Height); + DrawBackgroundDefault(Style.Background, Size, _rootControl.Location); + //DrawBackgroundSolidColor(Style.BackgroundColor, Width, Height); + DrawGridBackground(); } - internal static void DrawBackgroundDefault(Texture background, float width, float height) + internal static void DrawBackgroundSolidColor(Color color, float width, float height) + { + Rectangle backgroundRect = new Rectangle(0f, 0f, width, height); + Render2D.FillRectangle(backgroundRect, color); + } + + internal void DrawGridBackground() + { + var viewRect = GetClientArea(); + var upperLeft = _rootControl.PointFromParent(viewRect.Location); + var bottomRight = _rootControl.PointFromParent(viewRect.Size); + var min = Float2.Min(upperLeft, bottomRight); + var max = Float2.Max(upperLeft, bottomRight); + var pixelRange = (max - min) * ViewScale * 2.75f; + Render2D.PushClip(ref viewRect); + DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X); + DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y); + Render2D.PopClip(); + } + + private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange) + { + var linesColor = Style.BackgroundColor.RGBMultiplied(1.2f); + float[] gridTickStrengths = null; + Utilities.Utils.DrawCurveTicks((decimal tick, double step, float strength) => + { + var p = _rootControl.PointToParent(axis * (float)tick); ; + + // Draw line + var lineRect = new Rectangle + ( + viewRect.Location + (p - 0.5f) * axis, + Float2.Lerp(viewRect.Size, Float2.One, axis) + ); + Render2D.FillRectangle(lineRect, linesColor.AlphaMultiplied(strength)); + + }, Utilities.Utils.CurveTickSteps, ref gridTickStrengths, min, max, pixelRange); + } + + internal static void DrawBackgroundDefault(Texture background, Float2 size, Float2 offset) { if (background && background.ResidentMipLevels > 0) { var bSize = background.Size; - float bw = bSize.X; - float bh = bSize.Y; - var pos = Float2.Mod(bSize); + var pos = Float2.Mod(offset / bSize) * bSize; + var max = Float2.Ceil(size / bSize + 1.0f); if (pos.X > 0) - pos.X -= bw; + pos.X -= bSize.X; if (pos.Y > 0) - pos.Y -= bh; + pos.Y -= bSize.Y; - int maxI = Mathf.CeilToInt(width / bw + 1.0f); - int maxJ = Mathf.CeilToInt(height / bh + 1.0f); - - for (int i = 0; i < maxI; i++) + for (int i = 0; i < max.X; i++) { - for (int j = 0; j < maxJ; j++) + for (int j = 0; j < max.Y; j++) { - Render2D.DrawTexture(background, new Rectangle(pos.X + i * bw, pos.Y + j * bh, bw, bh), Color.White); + Render2D.DrawTexture(background, new Rectangle(pos.X + i * bSize.X, pos.Y + j * bSize.Y, bSize), Color.White); } } } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 9f326f36c..55c643f12 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -217,7 +217,7 @@ namespace FlaxEditor.Surface set { // Clamp - value = Mathf.Clamp(value, 0.05f, 1.6f); + value = Mathf.Clamp(value, 0.05f, 1.85f); // Check if value will change if (Mathf.Abs(value - _targetScale) > 0.0001f) @@ -511,7 +511,7 @@ namespace FlaxEditor.Surface { GroupID = Custom.GroupID, Name = "Custom", - Color = Color.Wheat + Color = Color.Wheat.RGBMultiplied(0.4f), }; } else diff --git a/Source/Editor/Undo/Actions/PasteActorsAction.cs b/Source/Editor/Undo/Actions/PasteActorsAction.cs index 2fe4e6235..f4dc7b318 100644 --- a/Source/Editor/Undo/Actions/PasteActorsAction.cs +++ b/Source/Editor/Undo/Actions/PasteActorsAction.cs @@ -41,7 +41,7 @@ namespace FlaxEditor.Actions ActionString = name; _pasteParent = pasteParent; - _idsMapping = new Dictionary(objectIds.Length * 4); + _idsMapping = new Dictionary(objectIds.Length); for (int i = 0; i < objectIds.Length; i++) { _idsMapping[objectIds[i]] = Guid.NewGuid(); @@ -72,13 +72,24 @@ namespace FlaxEditor.Actions /// /// Links the broken parent reference (missing parent). By default links the actor to the first scene. /// - /// The actor. - protected virtual void LinkBrokenParentReference(Actor actor) + /// The actor node. + protected virtual void LinkBrokenParentReference(ActorNode actorNode) { // Link to the first scene root if (Level.ScenesCount == 0) throw new Exception("Failed to paste actor with a broken reference. No loaded scenes."); - actor.SetParent(Level.GetScene(0), false); + actorNode.Actor.SetParent(Level.GetScene(0), false); + } + + /// + /// Checks if actor has a broken parent reference. For example, it's linked to the parent that is indie prefab editor while it should be pasted into scene. + /// + /// The actor node. + protected virtual void CheckBrokenParentReference(ActorNode actorNode) + { + // Ensure pasted object ends up on a scene + if (actorNode.Actor.Scene == null) + LinkBrokenParentReference(actorNode); } /// @@ -103,16 +114,13 @@ namespace FlaxEditor.Actions for (int i = 0; i < actors.Length; i++) { var actor = actors[i]; - - // Check if has no parent linked (broken reference eg. old parent not existing) - if (actor.Parent == null) - { - LinkBrokenParentReference(actor); - } - var node = GetNode(actor.ID); if (node is ActorNode actorNode) { + // Check if has no parent linked (broken reference eg. old parent not existing) + if (actor.Parent == null) + LinkBrokenParentReference(actorNode); + nodes.Add(actorNode); } } @@ -136,6 +144,12 @@ namespace FlaxEditor.Actions nodeParents[i].Actor.SetParent(pasteParentNode.Actor, false); } } + else + { + // Sanity check on pasted actor to ensure they end up i na proper context (scene editor or specific prefab editor) + foreach (var node in nodeParents) + CheckBrokenParentReference(node); + } // Store previously looked up names and the results Dictionary foundNamesResults = new(); diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 78ccedd60..fa5724780 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -166,7 +166,6 @@ namespace FlaxEditor.Viewport #endif // Input - internal bool _disableInputUpdate; private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown; private Float2 _startPos; diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index df10024cc..f171840b5 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -1,18 +1,19 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; using System.Collections.Generic; +using Object = FlaxEngine.Object; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Options; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Modes; +using FlaxEditor.Viewport.Widgets; using FlaxEditor.Windows; using FlaxEngine; using FlaxEngine.Gizmo; using FlaxEngine.GUI; -using Object = FlaxEngine.Object; namespace FlaxEditor.Viewport { @@ -26,6 +27,7 @@ namespace FlaxEditor.Viewport private readonly ContextMenuButton _showGridButton; private readonly ContextMenuButton _showNavigationButton; private readonly ContextMenuButton _toggleGameViewButton; + private readonly ContextMenuButton _showDirectionGizmoButton; private SelectionOutline _customSelectionOutline; /// @@ -226,12 +228,13 @@ namespace FlaxEditor.Viewport // Add rubber band selector _rubberBandSelector = new ViewportRubberBandSelector(this); - _directionGizmo = new DirectionGizmo(this); - _directionGizmo.AnchorPreset = AnchorPresets.TopRight; - _directionGizmo.Parent = this; - _directionGizmo.LocalY += 25; - _directionGizmo.LocalX -= 150; - _directionGizmo.Size = new Float2(150, 150); + + // Add direction gizmo + _directionGizmo = new DirectionGizmo(this) + { + AnchorPreset = AnchorPresets.TopRight, + Parent = this, + }; // Add grid Grid = new GridGizmo(this); @@ -252,11 +255,10 @@ namespace FlaxEditor.Viewport _showNavigationButton.CloseMenuOnClick = false; // Show direction gizmo widget - var showDirectionGizmoButton = ViewWidgetShowMenu.AddButton("Direction Gizmo", () => _directionGizmo.Visible = !_directionGizmo.Visible); - showDirectionGizmoButton.AutoCheck = true; - showDirectionGizmoButton.CloseMenuOnClick = false; - showDirectionGizmoButton.Checked = _directionGizmo.Visible; - + _showDirectionGizmoButton = ViewWidgetShowMenu.AddButton("Direction Gizmo", () => _directionGizmo.Visible = !_directionGizmo.Visible); + _showDirectionGizmoButton.AutoCheck = true; + _showDirectionGizmoButton.CloseMenuOnClick = false; + // Game View ViewWidgetButtonMenu.AddSeparator(); _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView); @@ -290,6 +292,18 @@ namespace FlaxEditor.Viewport // Game View InputActions.Add(options => options.ToggleGameView, ToggleGameView); + + editor.Options.OptionsChanged += OnEditorOptionsChanged; + OnEditorOptionsChanged(editor.Options.Options); + } + + private void OnEditorOptionsChanged(EditorOptions options) + { + _directionGizmo.Visible = options.Viewport.ShowDirectionGizmo; + _showDirectionGizmoButton.Checked = _directionGizmo.Visible; + _directionGizmo.Size = new Float2(DirectionGizmo.DefaultGizmoSize * options.Viewport.DirectionGizmoScale); + _directionGizmo.LocalX = -_directionGizmo.Size.X * 0.5f; + _directionGizmo.LocalY = _directionGizmo.Size.Y * 0.5f + ViewportWidgetsContainer.WidgetsHeight; } /// diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs b/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs index e6b71d5e9..32bf18b19 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs @@ -264,10 +264,17 @@ namespace FlaxEditor.Windows.Assets } /// - protected override void LinkBrokenParentReference(Actor actor) + protected override void LinkBrokenParentReference(ActorNode actorNode) { // Link to prefab root - actor.SetParent(_window.Graph.MainActor, false); + actorNode.Actor.SetParent(_window.Graph.MainActor, false); + } + + /// + protected override void CheckBrokenParentReference(ActorNode actorNode) + { + if (actorNode.Actor.Scene != null || actorNode.Root != _window.Graph.Root) + LinkBrokenParentReference(actorNode); } /// diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 40dd9131b..ad5a8caf2 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -58,7 +58,7 @@ namespace FlaxEditor.Windows cm.AddSeparator(); } - if (item is ContentFolder contentFolder && contentFolder.Node is ProjectTreeNode) + if (item is ContentFolder contentFolder && contentFolder.Node is ProjectFolderTreeNode) { cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); } @@ -76,8 +76,8 @@ namespace FlaxEditor.Windows cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path))); - if (!String.IsNullOrEmpty(Editor.Instance.Windows.ContentWin._itemsSearchBox.Text)) - { + if (!_showAllContentInTree && !String.IsNullOrEmpty(Editor.Instance.Windows.ContentWin._itemsSearchBox.Text)) + { cm.AddButton("Show in Content Panel", () => { Editor.Instance.Windows.ContentWin.ClearItemsSearch(); @@ -129,7 +129,7 @@ namespace FlaxEditor.Windows } } - if (isFolder && folder.Node is MainContentTreeNode) + if (isFolder && folder.Node is MainContentFolderTreeNode) { cm.AddSeparator(); } @@ -145,7 +145,7 @@ namespace FlaxEditor.Windows b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); - if (isFolder && folder.Node is MainContentTreeNode) + if (isFolder && folder.Node is MainContentFolderTreeNode) { // Do nothing } @@ -201,7 +201,7 @@ namespace FlaxEditor.Windows private void CreateNewModuleMenu(ContextMenu menu, ContentFolder folder, bool disableUncreatable = false) { // Check if is source folder to add new module - if (folder?.ParentFolder?.Node is ProjectTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) + if (folder?.ParentFolder?.Node is ProjectFolderTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) { var button = menu.AddButton("New module"); button.CloseMenuOnClick = false; @@ -216,7 +216,7 @@ namespace FlaxEditor.Windows private bool CanCreateFolder(ContentItem item = null) { - bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode); + bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode); return canCreateFolder; } diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs index 53dd6447b..4c7373aa6 100644 --- a/Source/Editor/Windows/ContentWindow.Navigation.cs +++ b/Source/Editor/Windows/ContentWindow.Navigation.cs @@ -10,15 +10,47 @@ namespace FlaxEditor.Windows { public partial class ContentWindow { - private static readonly List NavUpdateCache = new List(8); + private static readonly List NavUpdateCache = new List(8); private void OnTreeSelectionChanged(List from, List to) { + bool setLastViewFolder = !IsLayoutLocked; + if (!_showAllContentInTree && to.Count > 1) + { + _tree.Select(to[^1]); + return; + } + if (_showAllContentInTree && to.Count > 1) + { + if (setLastViewFolder) + { + var activeNode = GetActiveTreeSelection(to); + if (activeNode is ContentItemTreeNode itemNode) + SaveLastViewedFolder(itemNode.Item?.ParentFolder?.Node); + else + SaveLastViewedFolder(activeNode as ContentFolderTreeNode); + } + UpdateUI(); + return; + } + // Navigate - var source = from.Count > 0 ? from[0] as ContentTreeNode : null; - var target = to.Count > 0 ? to[0] as ContentTreeNode : null; + var source = from.Count > 0 ? from[0] as ContentFolderTreeNode : null; + var targetNode = GetActiveTreeSelection(to); + if (targetNode is ContentItemTreeNode itemNode2) + { + if (setLastViewFolder) + SaveLastViewedFolder(itemNode2.Item?.ParentFolder?.Node); + UpdateUI(); + itemNode2.Focus(); + return; + } + + var target = targetNode as ContentFolderTreeNode; Navigate(source, target); + if (setLastViewFolder) + SaveLastViewedFolder(target); target?.Focus(); } @@ -26,12 +58,12 @@ namespace FlaxEditor.Windows /// Navigates to the specified target content location. /// /// The target. - public void Navigate(ContentTreeNode target) + public void Navigate(ContentFolderTreeNode target) { Navigate(SelectedNode, target); } - private void Navigate(ContentTreeNode source, ContentTreeNode target) + private void Navigate(ContentFolderTreeNode source, ContentFolderTreeNode target) { if (target == null) target = _root; @@ -50,7 +82,8 @@ namespace FlaxEditor.Windows } // Show folder contents and select tree node - RefreshView(target); + if (!_showAllContentInTree) + RefreshView(target); _tree.Select(target); target.ExpandAllParents(); @@ -62,7 +95,8 @@ namespace FlaxEditor.Windows //UndoList.SetSize(32); // Update search - UpdateItemsSearch(); + if (!_showAllContentInTree) + UpdateItemsSearch(); // Unlock navigation _navigationUnlocked = true; @@ -81,7 +115,7 @@ namespace FlaxEditor.Windows if (_navigationUnlocked && _navigationUndo.Count > 0) { // Pop node - ContentTreeNode node = _navigationUndo.Pop(); + ContentFolderTreeNode node = _navigationUndo.Pop(); // Lock navigation _navigationUnlocked = false; @@ -90,7 +124,8 @@ namespace FlaxEditor.Windows _navigationRedo.Push(SelectedNode); // Select node - RefreshView(node); + if (!_showAllContentInTree) + RefreshView(node); _tree.Select(node); node.ExpandAllParents(); @@ -99,14 +134,16 @@ namespace FlaxEditor.Windows //UndoList.SetSize(32); // Update search - UpdateItemsSearch(); + if (!_showAllContentInTree) + UpdateItemsSearch(); // Unlock navigation _navigationUnlocked = true; // Update UI UpdateUI(); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); } } @@ -119,7 +156,7 @@ namespace FlaxEditor.Windows if (_navigationUnlocked && _navigationRedo.Count > 0) { // Pop node - ContentTreeNode node = _navigationRedo.Pop(); + ContentFolderTreeNode node = _navigationRedo.Pop(); // Lock navigation _navigationUnlocked = false; @@ -128,7 +165,8 @@ namespace FlaxEditor.Windows _navigationUndo.Push(SelectedNode); // Select node - RefreshView(node); + if (!_showAllContentInTree) + RefreshView(node); _tree.Select(node); node.ExpandAllParents(); @@ -137,14 +175,16 @@ namespace FlaxEditor.Windows //UndoList.SetSize(32); // Update search - UpdateItemsSearch(); + if (!_showAllContentInTree) + UpdateItemsSearch(); // Unlock navigation _navigationUnlocked = true; // Update UI UpdateUI(); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); } } @@ -153,8 +193,8 @@ namespace FlaxEditor.Windows /// public void NavigateUp() { - ContentTreeNode target = _root; - ContentTreeNode current = SelectedNode; + ContentFolderTreeNode target = _root; + ContentFolderTreeNode current = SelectedNode; if (current?.Folder.ParentFolder != null) { @@ -188,7 +228,7 @@ namespace FlaxEditor.Windows // Spawn buttons var nodes = NavUpdateCache; nodes.Clear(); - ContentTreeNode node = SelectedNode; + ContentFolderTreeNode node = SelectedNode; while (node != null) { nodes.Add(node); @@ -222,13 +262,31 @@ namespace FlaxEditor.Windows /// /// Gets the selected tree node. /// - public ContentTreeNode SelectedNode => _tree.SelectedNode as ContentTreeNode; + public ContentFolderTreeNode SelectedNode + { + get + { + var selected = GetActiveTreeSelection(_tree.Selection); + if (selected is ContentItemTreeNode itemNode) + return itemNode.Parent as ContentFolderTreeNode; + return selected as ContentFolderTreeNode; + } + } /// /// Gets the current view folder. /// public ContentFolder CurrentViewFolder => SelectedNode?.Folder; + private TreeNode GetActiveTreeSelection(List selection) + { + if (selection == null || selection.Count == 0) + return null; + return _showAllContentInTree && selection.Count > 1 + ? selection[^1] + : selection[0]; + } + /// /// Shows the root folder. /// @@ -236,5 +294,10 @@ namespace FlaxEditor.Windows { _tree.Select(_root); } + + private void SaveLastViewedFolder(ContentFolderTreeNode node) + { + Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, node?.Path ?? string.Empty); + } } } diff --git a/Source/Editor/Windows/ContentWindow.Search.cs b/Source/Editor/Windows/ContentWindow.Search.cs index 5a0ed63aa..af6cc67ef 100644 --- a/Source/Editor/Windows/ContentWindow.Search.cs +++ b/Source/Editor/Windows/ContentWindow.Search.cs @@ -115,12 +115,14 @@ namespace FlaxEditor.Windows var root = _root; root.LockChildrenRecursive(); + _suppressExpandedStateSave = true; PerformLayout(); // Update tree var query = _foldersSearchBox.Text; root.UpdateFilter(query); + _suppressExpandedStateSave = false; root.UnlockChildrenRecursive(); PerformLayout(); PerformLayout(); @@ -161,6 +163,11 @@ namespace FlaxEditor.Windows // Skip events during setup or init stuff if (IsLayoutLocked) return; + if (_showAllContentInTree) + { + RefreshTreeItems(); + return; + } // Check if clear filters if (_itemsSearchBox.TextLength == 0 && !_viewDropdown.HasSelection) @@ -200,7 +207,7 @@ namespace FlaxEditor.Windows // Special case for root folder for (int i = 0; i < _root.ChildrenCount; i++) { - if (_root.GetChild(i) is ContentTreeNode node) + if (_root.GetChild(i) is ContentFolderTreeNode node) UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles); } } @@ -222,7 +229,7 @@ namespace FlaxEditor.Windows // Special case for root folder for (int i = 0; i < _root.ChildrenCount; i++) { - if (_root.GetChild(i) is ContentTreeNode node) + if (_root.GetChild(i) is ContentFolderTreeNode node) UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles, query); } } diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 6313a91b0..2141b283e 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -27,10 +27,15 @@ namespace FlaxEditor.Windows public sealed partial class ContentWindow : EditorWindow { private const string ProjectDataLastViewedFolder = "LastViewedFolder"; + private const string ProjectDataExpandedFolders = "ExpandedFolders"; private bool _isWorkspaceDirty; private string _workspaceRebuildLocation; private string _lastViewedFolderBeforeReload; private SplitPanel _split; + private Panel _treeOnlyPanel; + private ContainerControl _treePanelRoot; + private ContainerControl _treeHeaderPanel; + private Panel _contentItemsSearchPanel; private Panel _contentViewPanel; private Panel _contentTreePanel; private ContentView _view; @@ -43,18 +48,23 @@ namespace FlaxEditor.Windows private readonly ToolStripButton _navigateUpButton; private NavigationBar _navigationBar; + private Panel _viewDropdownPanel; private Tree _tree; private TextBox _foldersSearchBox; private TextBox _itemsSearchBox; private ViewDropdown _viewDropdown; private SortType _sortType; private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true, _showGeneratedFiles = false; + private bool _showAllContentInTree; + private bool _suppressExpandedStateSave; + private readonly HashSet _expandedFolderPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + private bool _renameInTree; - private RootContentTreeNode _root; + private RootContentFolderTreeNode _root; private bool _navigationUnlocked; - private readonly Stack _navigationUndo = new Stack(32); - private readonly Stack _navigationRedo = new Stack(32); + private readonly Stack _navigationUndo = new Stack(32); + private readonly Stack _navigationRedo = new Stack(32); private NewItem _newElement; @@ -134,6 +144,9 @@ namespace FlaxEditor.Windows } } + internal bool IsTreeOnlyMode => _showAllContentInTree; + internal SortType CurrentSortType => _sortType; + /// /// Initializes a new instance of the class. /// @@ -163,14 +176,6 @@ namespace FlaxEditor.Windows _navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up."); _toolStrip.AddSeparator(); - // Navigation bar - _navigationBar = new NavigationBar - { - Parent = _toolStrip, - ScrollbarTrackColor = style.Background, - ScrollbarThumbColor = style.ForegroundGrey, - }; - // Split panel _split = new SplitPanel(options.Options.Interface.ContentWindowOrientation, ScrollBars.None, ScrollBars.None) { @@ -180,19 +185,38 @@ namespace FlaxEditor.Windows Parent = this, }; + // Tree-only panel (used when showing all content in the tree) + _treeOnlyPanel = new Panel(ScrollBars.None) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = new Margin(0, 0, _toolStrip.Bottom, 0), + Visible = false, + Parent = this, + }; + + // Tree host panel + _treePanelRoot = new ContainerControl + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = _split.Panel1, + }; + // Content structure tree searching query input box - var headerPanel = new ContainerControl + _treeHeaderPanel = new ContainerControl { AnchorPreset = AnchorPresets.HorizontalStretchTop, BackgroundColor = style.Background, IsScrollable = false, Offsets = new Margin(0, 0, 0, 18 + 6), + Parent = _treePanelRoot, }; + _foldersSearchBox = new SearchBox { AnchorPreset = AnchorPresets.HorizontalStretchMiddle, - Parent = headerPanel, - Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18), + Parent = _treeHeaderPanel, + Bounds = new Rectangle(4, 4, _treeHeaderPanel.Width - 8, 18), }; _foldersSearchBox.TextChanged += OnFoldersSearchBoxTextChanged; @@ -200,55 +224,74 @@ namespace FlaxEditor.Windows _contentTreePanel = new Panel { AnchorPreset = AnchorPresets.StretchAll, - Offsets = new Margin(0, 0, headerPanel.Bottom, 0), + Offsets = new Margin(0, 0, _treeHeaderPanel.Bottom, 0), IsScrollable = true, ScrollBars = ScrollBars.Both, - Parent = _split.Panel1, + Parent = _treePanelRoot, }; // Content structure tree - _tree = new Tree(false) + _tree = new Tree(true) { DrawRootTreeLine = false, Parent = _contentTreePanel, }; _tree.SelectedChanged += OnTreeSelectionChanged; - headerPanel.Parent = _split.Panel1; // Content items searching query input box and filters selector - var contentItemsSearchPanel = new ContainerControl + _contentItemsSearchPanel = new Panel { AnchorPreset = AnchorPresets.HorizontalStretchTop, IsScrollable = true, Offsets = new Margin(0, 0, 0, 18 + 8), Parent = _split.Panel2, }; - const float viewDropdownWidth = 50.0f; + _itemsSearchBox = new SearchBox { AnchorPreset = AnchorPresets.HorizontalStretchMiddle, - Parent = contentItemsSearchPanel, - Bounds = new Rectangle(viewDropdownWidth + 8, 4, contentItemsSearchPanel.Width - 12 - viewDropdownWidth, 18), + Parent = _contentItemsSearchPanel, + Bounds = new Rectangle(4, 4, _contentItemsSearchPanel.Width - 8, 18), }; _itemsSearchBox.TextChanged += UpdateItemsSearch; + + _viewDropdownPanel = new Panel + { + Width = 50.0f, + Parent = this, + AnchorPreset = AnchorPresets.TopLeft, + BackgroundColor = Color.Transparent, + }; + _viewDropdown = new ViewDropdown { - AnchorPreset = AnchorPresets.MiddleLeft, SupportMultiSelect = true, TooltipText = "Change content view and filter options", - Parent = contentItemsSearchPanel, - Offsets = new Margin(4, viewDropdownWidth, -9, 18), + Offsets = Margin.Zero, + Width = 46.0f, + Height = 18.0f, + Parent = _viewDropdownPanel, }; + _viewDropdown.LocalX += 2.0f; + _viewDropdown.LocalY += _toolStrip.ItemsHeight * 0.5f - 9.0f; _viewDropdown.SelectedIndexChanged += e => UpdateItemsSearch(); for (int i = 0; i <= (int)ContentItemSearchFilter.Other; i++) _viewDropdown.Items.Add(((ContentItemSearchFilter)i).ToString()); _viewDropdown.PopupCreate += OnViewDropdownPopupCreate; + // Navigation bar (after view dropdown so layout order stays stable) + _navigationBar = new NavigationBar + { + Parent = _toolStrip, + ScrollbarTrackColor = style.Background, + ScrollbarThumbColor = style.ForegroundGrey, + }; + // Content view panel _contentViewPanel = new Panel { AnchorPreset = AnchorPresets.StretchAll, - Offsets = new Margin(0, 0, contentItemsSearchPanel.Bottom + 4, 0), + Offsets = new Margin(0, 0, _contentItemsSearchPanel.Bottom + 4, 0), IsScrollable = true, ScrollBars = ScrollBars.Vertical, Parent = _split.Panel2, @@ -268,9 +311,14 @@ namespace FlaxEditor.Windows _view.OnDelete += Delete; _view.OnDuplicate += Duplicate; _view.OnPaste += Paste; + _view.ViewScaleChanged += ApplyTreeViewScale; _view.InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus()); InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus()); + + LoadExpandedFolders(); + UpdateViewDropdownBounds(); + ApplyTreeViewScale(); } private void OnCreateNewItemButtonClicked() @@ -325,6 +373,7 @@ namespace FlaxEditor.Windows var viewType = menu.AddChildMenu("View Type"); viewType.ContextMenu.AddButton("Tiles", OnViewTypeButtonClicked).Tag = ContentViewType.Tiles; viewType.ContextMenu.AddButton("List", OnViewTypeButtonClicked).Tag = ContentViewType.List; + viewType.ContextMenu.AddButton("Tree View", OnViewTypeButtonClicked).Tag = "Tree"; viewType.ContextMenu.VisibleChanged += control => { if (!control.Visible) @@ -332,13 +381,23 @@ namespace FlaxEditor.Windows foreach (var item in ((ContextMenu)control).Items) { if (item is ContextMenuButton button) - button.Checked = View.ViewType == (ContentViewType)button.Tag; + { + if (button.Tag is ContentViewType type) + button.Checked = View.ViewType == type && !_showAllContentInTree; + else + button.Checked = _showAllContentInTree; + } } }; var show = menu.AddChildMenu("Show"); { - var b = show.ContextMenu.AddButton("File extensions", () => View.ShowFileExtensions = !View.ShowFileExtensions); + var b = show.ContextMenu.AddButton("File extensions", () => + { + View.ShowFileExtensions = !View.ShowFileExtensions; + if (_showAllContentInTree) + UpdateTreeItemNames(_root); + }); b.TooltipText = "Shows all files with extensions"; b.Checked = View.ShowFileExtensions; b.CloseMenuOnClick = false; @@ -419,9 +478,63 @@ namespace FlaxEditor.Windows RefreshView(); } + private void SetShowAllContentInTree(bool value) + { + if (_showAllContentInTree == value) + return; + + _showAllContentInTree = value; + ApplyTreeViewMode(); + } + + private void ApplyTreeViewMode() + { + if (_treeOnlyPanel == null || _split == null || _treePanelRoot == null) + return; + + if (_showAllContentInTree) + { + _split.Visible = false; + _treeOnlyPanel.Visible = true; + _treePanelRoot.Parent = _treeOnlyPanel; + _treePanelRoot.Offsets = Margin.Zero; + _contentItemsSearchPanel.Visible = false; + _itemsSearchBox.Visible = false; + _contentViewPanel.Visible = false; + RefreshTreeItems(); + } + else + { + _treeOnlyPanel.Visible = false; + _split.Visible = true; + _treePanelRoot.Parent = _split.Panel1; + _treePanelRoot.Offsets = Margin.Zero; + _contentItemsSearchPanel.Visible = true; + _itemsSearchBox.Visible = true; + _contentViewPanel.Visible = true; + if (_tree.SelectedNode is ContentItemTreeNode itemNode && itemNode.Parent is TreeNode parentNode) + _tree.Select(parentNode); + if (_root != null) + RemoveTreeAssetNodes(_root); + RefreshView(SelectedNode); + } + + PerformLayout(); + ApplyTreeViewScale(); + _tree.PerformLayout(); + } + private void OnViewTypeButtonClicked(ContextMenuButton button) { - View.ViewType = (ContentViewType)button.Tag; + if (button.Tag is ContentViewType viewType) + { + SetShowAllContentInTree(false); + View.ViewType = viewType; + } + else + { + SetShowAllContentInTree(true); + } } private void OnFilterClicked(ContextMenuButton filterButton) @@ -481,15 +594,58 @@ namespace FlaxEditor.Windows // Show element in the view Select(item, true); - // Disable scrolling in content view - if (_contentViewPanel.VScrollBar != null) - _contentViewPanel.VScrollBar.ThumbEnabled = false; - if (_contentViewPanel.HScrollBar != null) - _contentViewPanel.HScrollBar.ThumbEnabled = false; - ScrollingOnContentView(false); + // Disable scrolling in proper view + _renameInTree = _showAllContentInTree; + if (_renameInTree) + { + if (_contentTreePanel.VScrollBar != null) + _contentTreePanel.VScrollBar.ThumbEnabled = false; + if (_contentTreePanel.HScrollBar != null) + _contentTreePanel.HScrollBar.ThumbEnabled = false; + ScrollingOnTreeView(false); + } + else + { + if (_contentViewPanel.VScrollBar != null) + _contentViewPanel.VScrollBar.ThumbEnabled = false; + if (_contentViewPanel.HScrollBar != null) + _contentViewPanel.HScrollBar.ThumbEnabled = false; + ScrollingOnContentView(false); + } // Show rename popup - var popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true); + RenamePopup popup; + if (_renameInTree) + { + TreeNode node = null; + if (item is ContentFolder folder) + node = folder.Node; + else if (item.ParentFolder != null) + node = FindTreeItemNode(item.ParentFolder.Node, item); + if (node == null) + { + // Fallback to content view rename + popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true); + } + else + { + var area = node.TextRect; + const float minRenameWidth = 220.0f; + if (area.Width < minRenameWidth) + { + float expand = minRenameWidth - area.Width; + area.X -= expand * 0.5f; + area.Width = minRenameWidth; + } + area.Y -= 2; + area.Height += 4.0f; + popup = RenamePopup.Show(node, area, item.ShortName, true); + } + } + else + { + popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true); + } popup.Tag = item; popup.Validate += OnRenameValidate; popup.Renamed += renamePopup => Rename((ContentItem)renamePopup.Tag, renamePopup.Text); @@ -509,12 +665,24 @@ namespace FlaxEditor.Windows private void OnRenameClosed(RenamePopup popup) { - // Restore scrolling in content view - if (_contentViewPanel.VScrollBar != null) - _contentViewPanel.VScrollBar.ThumbEnabled = true; - if (_contentViewPanel.HScrollBar != null) - _contentViewPanel.HScrollBar.ThumbEnabled = true; - ScrollingOnContentView(true); + // Restore scrolling in proper view + if (_renameInTree) + { + if (_contentTreePanel.VScrollBar != null) + _contentTreePanel.VScrollBar.ThumbEnabled = true; + if (_contentTreePanel.HScrollBar != null) + _contentTreePanel.HScrollBar.ThumbEnabled = true; + ScrollingOnTreeView(true); + } + else + { + if (_contentViewPanel.VScrollBar != null) + _contentViewPanel.VScrollBar.ThumbEnabled = true; + if (_contentViewPanel.HScrollBar != null) + _contentViewPanel.HScrollBar.ThumbEnabled = true; + ScrollingOnContentView(true); + } + _renameInTree = false; // Check if was creating new element if (_newElement != null) @@ -927,6 +1095,16 @@ namespace FlaxEditor.Windows } } + private void OnContentDatabaseItemAdded(ContentItem contentItem) + { + if (contentItem is ContentFolder folder && _expandedFolderPaths.Contains(StringUtils.NormalizePath(folder.Path))) + { + _suppressExpandedStateSave = true; + folder.Node?.Expand(true); + _suppressExpandedStateSave = false; + } + } + /// /// Opens the specified content item. /// @@ -943,7 +1121,8 @@ namespace FlaxEditor.Windows var folder = (ContentFolder)item; folder.Node.Expand(); _tree.Select(folder.Node); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); return; } @@ -984,6 +1163,36 @@ namespace FlaxEditor.Windows // Ensure that window is visible FocusOrShow(); + if (_showAllContentInTree) + { + var targetNode = item is ContentFolder folder ? folder.Node : parent.Node; + if (targetNode != null) + { + targetNode.ExpandAllParents(); + if (item is ContentFolder) + { + _tree.Select(targetNode); + _contentTreePanel.ScrollViewTo(targetNode, fastScroll); + targetNode.Focus(); + } + else + { + var itemNode = FindTreeItemNode(targetNode, item); + if (itemNode != null) + { + _tree.Select(itemNode); + _contentTreePanel.ScrollViewTo(itemNode, fastScroll); + itemNode.Focus(); + } + else + { + _tree.Select(targetNode); + } + } + } + return; + } + // Navigate to the parent directory Navigate(parent.Node); @@ -995,12 +1204,26 @@ namespace FlaxEditor.Windows _view.Focus(); } + private ContentItemTreeNode FindTreeItemNode(ContentFolderTreeNode parentNode, ContentItem item) + { + if (parentNode == null || item == null) + return null; + for (int i = 0; i < parentNode.ChildrenCount; i++) + { + if (parentNode.GetChild(i) is ContentItemTreeNode itemNode && itemNode.Item == item) + return itemNode; + } + return null; + } + /// /// Refreshes the current view items collection. /// public void RefreshView() { - if (_view.IsSearching) + if (_showAllContentInTree) + RefreshTreeItems(); + else if (_view.IsSearching) UpdateItemsSearch(); else RefreshView(SelectedNode); @@ -1010,8 +1233,14 @@ namespace FlaxEditor.Windows /// Refreshes the view. /// /// The target location. - public void RefreshView(ContentTreeNode target) + public void RefreshView(ContentFolderTreeNode target) { + if (_showAllContentInTree) + { + RefreshTreeItems(); + return; + } + _view.IsSearching = false; if (target == _root) { @@ -1019,7 +1248,7 @@ namespace FlaxEditor.Windows var items = new List(8); for (int i = 0; i < _root.ChildrenCount; i++) { - if (_root.GetChild(i) is ContentTreeNode node) + if (_root.GetChild(i) is ContentFolderTreeNode node) { items.Add(node.Folder); } @@ -1038,12 +1267,263 @@ namespace FlaxEditor.Windows } } + private void RefreshTreeItems() + { + if (!_showAllContentInTree || _root == null) + return; + + _root.LockChildrenRecursive(); + RemoveTreeAssetNodes(_root); + AddTreeAssetNodes(_root); + _root.UnlockChildrenRecursive(); + _tree.PerformLayout(); + } + + private void UpdateTreeItemNames(ContentFolderTreeNode node) + { + if (node == null) + return; + + for (int i = 0; i < node.ChildrenCount; i++) + { + if (node.GetChild(i) is ContentFolderTreeNode childFolder) + { + UpdateTreeItemNames(childFolder); + } + else if (node.GetChild(i) is ContentItemTreeNode itemNode) + { + itemNode.UpdateDisplayedName(); + } + } + } + + internal void OnContentTreeNodeExpandedChanged(ContentFolderTreeNode node, bool isExpanded) + { + if (_suppressExpandedStateSave || node == null || node == _root) + return; + + var path = node.Path; + if (string.IsNullOrEmpty(path)) + return; + path = StringUtils.NormalizePath(path); + + if (isExpanded) + _expandedFolderPaths.Add(path); + else + // Remove all sub paths if parent folder is closed. + _expandedFolderPaths.RemoveWhere(x => x.Contains(path)); + + SaveExpandedFolders(); + } + + internal void TryAutoExpandContentNode(ContentFolderTreeNode node) + { + if (node == null || node == _root) + return; + + var path = node.Path; + if (string.IsNullOrEmpty(path)) + return; + path = StringUtils.NormalizePath(path); + + if (!_expandedFolderPaths.Contains(path)) + return; + + _suppressExpandedStateSave = true; + node.Expand(true); + _suppressExpandedStateSave = false; + } + + private void LoadExpandedFolders() + { + _expandedFolderPaths.Clear(); + if (Editor.ProjectCache.TryGetCustomData(ProjectDataExpandedFolders, out string data) && !string.IsNullOrWhiteSpace(data)) + { + var entries = data.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < entries.Length; i++) + { + var path = entries[i].Trim(); + if (path.Length == 0) + continue; + _expandedFolderPaths.Add(StringUtils.NormalizePath(path)); + } + } + } + + private void SaveExpandedFolders() + { + if (_expandedFolderPaths.Count == 0) + { + Editor.ProjectCache.RemoveCustomData(ProjectDataExpandedFolders); + return; + } + + var data = string.Join("\n", _expandedFolderPaths); + Editor.ProjectCache.SetCustomData(ProjectDataExpandedFolders, data); + } + + private void ApplyExpandedFolders() + { + if (_root == null || _expandedFolderPaths.Count == 0) + return; + + _suppressExpandedStateSave = true; + foreach (var path in _expandedFolderPaths) + { + if (Editor.ContentDatabase.Find(path) is ContentFolder folder) + { + folder.Node.ExpandAllParents(true); + folder.Node.Expand(true); + } + } + _suppressExpandedStateSave = false; + } + + private void RemoveTreeAssetNodes(ContentFolderTreeNode node) + { + for (int i = node.ChildrenCount - 1; i >= 0; i--) + { + if (node.GetChild(i) is ContentItemTreeNode itemNode) + { + node.RemoveChild(itemNode); + itemNode.Dispose(); + } + else if (node.GetChild(i) is ContentFolderTreeNode childFolder) + { + RemoveTreeAssetNodes(childFolder); + } + } + } + + private void AddTreeAssetNodes(ContentFolderTreeNode node) + { + if (node.Folder != null) + { + var children = node.Folder.Children; + for (int i = 0; i < children.Count; i++) + { + var child = children[i]; + if (child is ContentFolder) + continue; + if (!ShouldShowTreeItem(child)) + continue; + + var itemNode = new ContentItemTreeNode(child) + { + Parent = node, + }; + } + } + + for (int i = 0; i < node.ChildrenCount; i++) + { + if (node.GetChild(i) is ContentFolderTreeNode childFolder) + { + AddTreeAssetNodes(childFolder); + } + } + + node.SortChildren(); + } + + private bool ShouldShowTreeItem(ContentItem item) + { + if (item == null || !item.Visible) + return false; + if (_viewDropdown != null && _viewDropdown.HasSelection) + { + var filterIndex = (int)item.SearchFilter; + if (!_viewDropdown.Selection.Contains(filterIndex)) + return false; + } + if (!_showAllFiles && item is FileItem) + return false; + if (!_showGeneratedFiles && IsGeneratedFile(item.Path)) + return false; + return true; + } + + private static bool IsGeneratedFile(string path) + { + return path.EndsWith(".Gen.cs", StringComparison.Ordinal) || + path.EndsWith(".Gen.h", StringComparison.Ordinal) || + path.EndsWith(".Gen.cpp", StringComparison.Ordinal) || + path.EndsWith(".csproj", StringComparison.Ordinal) || + path.Contains(".CSharp"); + } + private void UpdateUI() { UpdateToolstrip(); UpdateNavigationBar(); } + private void ApplyTreeViewScale() + { + if (_tree == null) + return; + + var scale = _showAllContentInTree ? View.ViewScale : 1.0f; + var headerHeight = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var style = Style.Current; + var fontSize = Mathf.Clamp(style.FontSmall.Size * scale, 8.0f, 28.0f); + var fontRef = new FontReference(style.FontSmall.Asset, fontSize); + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var textMarginLeft = 2.0f + Mathf.Max(0.0f, iconSize - 16.0f); + ApplyTreeNodeScale(_root, headerHeight, fontRef, textMarginLeft); + _root?.PerformLayout(true); + _tree.PerformLayout(); + } + + private void ApplyTreeNodeScale(ContentFolderTreeNode node, float headerHeight, FontReference fontRef, float textMarginLeft) + { + if (node == null) + return; + + var margin = node.TextMargin; + margin.Left = textMarginLeft; + margin.Top = 2.0f; + margin.Right = 2.0f; + margin.Bottom = 2.0f; + node.TextMargin = margin; + node.CustomArrowRect = GetTreeArrowRect(node, headerHeight); + node.HeaderHeight = headerHeight; + node.TextFont = fontRef; + for (int i = 0; i < node.ChildrenCount; i++) + { + if (node.GetChild(i) is ContentFolderTreeNode child) + ApplyTreeNodeScale(child, headerHeight, fontRef, textMarginLeft); + else if (node.GetChild(i) is ContentItemTreeNode itemNode) + { + var itemMargin = itemNode.TextMargin; + itemMargin.Left = textMarginLeft; + itemMargin.Top = 2.0f; + itemMargin.Right = 2.0f; + itemMargin.Bottom = 2.0f; + itemNode.TextMargin = itemMargin; + itemNode.HeaderHeight = headerHeight; + itemNode.TextFont = fontRef; + } + } + } + + private static Rectangle GetTreeArrowRect(ContentFolderTreeNode node, float headerHeight) + { + if (node == null) + return Rectangle.Empty; + + var scale = Editor.Instance?.Windows?.ContentWin?.IsTreeOnlyMode == true + ? Editor.Instance.Windows.ContentWin.View.ViewScale + : 1.0f; + var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f); + var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f); + var textRect = node.TextRect; + var iconLeft = textRect.Left - iconSize - 2.0f; + var x = iconLeft - arrowSize - 2.0f; + var y = (headerHeight - arrowSize) * 0.5f; + return new Rectangle(Mathf.Max(x, 0.0f), Mathf.Max(y, 0.0f), arrowSize, arrowSize); + } + private void UpdateToolstrip() { if (_toolStrip == null) @@ -1063,20 +1543,42 @@ namespace FlaxEditor.Windows { var bottomPrev = _toolStrip.Bottom; _navigationBar.UpdateBounds(_toolStrip); + if (_viewDropdownPanel != null && _viewDropdownPanel.Visible) + { + var reserved = _viewDropdownPanel.Width + 8.0f; + _navigationBar.Width = Mathf.Max(_navigationBar.Width - reserved, 0.0f); + } if (bottomPrev != _toolStrip.Bottom) { // Navigation bar changed toolstrip height _split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0); + if (_treeOnlyPanel != null) + _treeOnlyPanel.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0); PerformLayout(); } + UpdateViewDropdownBounds(); } } + private void UpdateViewDropdownBounds() + { + if (_viewDropdownPanel == null || _toolStrip == null) + return; + + var margin = _toolStrip.ItemsMargin; + var height = _toolStrip.ItemsHeight; + var y = _toolStrip.Y + (_toolStrip.Height - height) * 0.5f; + var width = _viewDropdownPanel.Width; + var x = _toolStrip.Right - width - margin.Right; + _viewDropdownPanel.Bounds = new Rectangle(x, y, width, height); + } + /// public override void OnInit() { // Content database events Editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true; + Editor.ContentDatabase.ItemAdded += OnContentDatabaseItemAdded; Editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved; Editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; }; Editor.ContentDatabase.WorkspaceRebuilt += () => @@ -1095,6 +1597,7 @@ namespace FlaxEditor.Windows ShowRoot(); }; + LoadExpandedFolders(); Refresh(); // Load last viewed folder @@ -1110,7 +1613,7 @@ namespace FlaxEditor.Windows private void OnScriptsReloadBegin() { - var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null; + var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentFolderTreeNode : null; _lastViewedFolderBeforeReload = lastViewedFolder?.Path ?? string.Empty; _tree.RemoveChild(_root); @@ -1133,7 +1636,7 @@ namespace FlaxEditor.Windows private void Refresh() { // Setup content root node - _root = new RootContentTreeNode + _root = new RootContentFolderTreeNode { ChildrenIndent = 0 }; @@ -1156,7 +1659,7 @@ namespace FlaxEditor.Windows _root.AddChild(Editor.ContentDatabase.Engine); Editor.ContentDatabase.Game?.Expand(true); - _tree.Margin = new Margin(0.0f, 0.0f, -16.0f, 2.0f); // Hide root node + _tree.Margin = new Margin(0.0f, 0.0f, -16.0f, ScrollBar.DefaultSize + 2); // Hide root node _tree.AddChild(_root); // Setup navigation @@ -1167,6 +1670,8 @@ namespace FlaxEditor.Windows // Update UI layout _isLayoutLocked = false; PerformLayout(); + ApplyExpandedFolders(); + ApplyTreeViewMode(); } /// @@ -1176,7 +1681,10 @@ namespace FlaxEditor.Windows if (_isWorkspaceDirty) { _isWorkspaceDirty = false; - RefreshView(); + if (_showAllContentInTree) + RefreshTreeItems(); + else + RefreshView(); } base.Update(deltaTime); @@ -1186,7 +1694,15 @@ namespace FlaxEditor.Windows public override void OnExit() { // Save last viewed folder - var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null; + ContentFolderTreeNode lastViewedFolder = null; + if (_tree.Selection.Count == 1) + { + var selectedNode = _tree.SelectedNode; + if (selectedNode is ContentItemTreeNode itemNode) + lastViewedFolder = itemNode.Item?.ParentFolder?.Node; + else + lastViewedFolder = selectedNode as ContentFolderTreeNode; + } Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, lastViewedFolder?.Path ?? string.Empty); // Clear view @@ -1197,7 +1713,7 @@ namespace FlaxEditor.Windows { while (_root.HasChildren) { - _root.RemoveChild((ContentTreeNode)_root.GetChild(0)); + _root.RemoveChild((ContentFolderTreeNode)_root.GetChild(0)); } } } @@ -1232,7 +1748,12 @@ namespace FlaxEditor.Windows { ShowContextMenuForItem(null, ref location, false); } - else if (c is ContentTreeNode node) + else if (c is ContentItemTreeNode itemNode) + { + _tree.Select(itemNode); + ShowContextMenuForItem(itemNode.Item, ref location, false); + } + else if (c is ContentFolderTreeNode node) { _tree.Select(node); ShowContextMenuForItem(node.Folder, ref location, true); @@ -1258,11 +1779,16 @@ namespace FlaxEditor.Windows /// protected override void PerformLayoutBeforeChildren() { - UpdateNavigationBarBounds(); - base.PerformLayoutBeforeChildren(); } + /// + protected override void PerformLayoutAfterChildren() + { + base.PerformLayoutAfterChildren(); + UpdateNavigationBarBounds(); + } + /// public override bool UseLayoutData => true; @@ -1277,6 +1803,7 @@ namespace FlaxEditor.Windows writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString()); writer.WriteAttributeString("ShowGeneratedFiles", ShowGeneratedFiles.ToString()); writer.WriteAttributeString("ViewType", _view.ViewType.ToString()); + writer.WriteAttributeString("TreeViewAllContent", _showAllContentInTree.ToString()); } /// @@ -1297,6 +1824,9 @@ namespace FlaxEditor.Windows ShowGeneratedFiles = value2; if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType)) _view.ViewType = viewType; + if (bool.TryParse(node.GetAttribute("TreeViewAllContent"), out value2)) + _showAllContentInTree = value2; + ApplyTreeViewMode(); } /// @@ -1304,6 +1834,7 @@ namespace FlaxEditor.Windows { _split.SplitterValue = 0.2f; _view.ViewScale = 1.0f; + _showAllContentInTree = false; } /// @@ -1312,10 +1843,17 @@ namespace FlaxEditor.Windows _foldersSearchBox = null; _itemsSearchBox = null; _viewDropdown = null; + _viewDropdownPanel = null; + _treePanelRoot = null; + _treeHeaderPanel = null; + _treeOnlyPanel = null; + _contentItemsSearchPanel = null; Editor.Options.OptionsChanged -= OnOptionsChanged; ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; + if (Editor?.ContentDatabase != null) + Editor.ContentDatabase.ItemAdded -= OnContentDatabaseItemAdded; base.OnDestroy(); } diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp index 455b6ae3d..f583ab32a 100644 --- a/Source/Engine/Content/BinaryAsset.cpp +++ b/Source/Engine/Content/BinaryAsset.cpp @@ -84,6 +84,11 @@ bool BinaryAsset::Init(AssetInitData& initData) { asset->_dependantAssets.Add(this); } + else + { + // Dependency is not yet loaded to keep track this link to act when it's loaded + Content::onAssetDepend(this, e.First); + } } #endif @@ -336,9 +341,7 @@ bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool // Force-resolve storage (asset at that path could be not yet loaded into registry) storage = ContentStorageManager::GetStorage(filePath); } - - // Check if can perform write operation to the asset container - if (storage && !storage->AllowDataModifications()) + if (storage && storage->IsReadOnly()) { LOG(Warning, "Cannot write to the asset storage container."); return true; @@ -635,7 +638,7 @@ void BinaryAsset::onRename(const StringView& newPath) ScopeLock lock(Locker); // We don't support packages now - ASSERT(!Storage->IsPackage() && Storage->AllowDataModifications() && Storage->GetEntriesCount() == 1); + ASSERT(!Storage->IsPackage() && !Storage->IsReadOnly() && Storage->GetEntriesCount() == 1); // Rename storage Storage->OnRename(newPath); diff --git a/Source/Engine/Content/BinaryAsset.h b/Source/Engine/Content/BinaryAsset.h index 39c34588e..e52e9dcff 100644 --- a/Source/Engine/Content/BinaryAsset.h +++ b/Source/Engine/Content/BinaryAsset.h @@ -22,6 +22,7 @@ API_CLASS(Abstract, NoSpawn) class FLAXENGINE_API BinaryAsset : public Asset { DECLARE_ASSET_HEADER(BinaryAsset); + friend Content; protected: AssetHeader _header; FlaxStorageReference _storageRef; // Allow asset to have missing storage reference but only before asset is loaded or if it's virtual diff --git a/Source/Engine/Content/Cache/AssetsCache.cpp b/Source/Engine/Content/Cache/AssetsCache.cpp index 2a2d252b6..96083509e 100644 --- a/Source/Engine/Content/Cache/AssetsCache.cpp +++ b/Source/Engine/Content/Cache/AssetsCache.cpp @@ -415,9 +415,7 @@ void AssetsCache::RegisterAssets(FlaxStorage* storage) // Check if need to resolve any collisions if (duplicatedEntries.HasItems()) { - // Check if cannot resolve collision for that container (it must allow to write to) - // TODO: we could support packages as well but don't have to do it now, maybe in future - if (storage->AllowDataModifications() == false) + if (storage->IsReadOnly()) { LOG(Error, "Cannot register \'{0}\'. Founded duplicated asset at \'{1}\' but storage container doesn't allow data modifications.", storagePath, info.Path); return; diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 47ec97c0a..39a8ceca6 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -98,6 +98,9 @@ namespace DateTime LastWorkspaceDiscovery; CriticalSection WorkspaceDiscoveryLocker; #endif +#if USE_EDITOR + Dictionary> PendingDependencies; +#endif } #if ENABLE_ASSETS_DISCOVERY @@ -184,6 +187,9 @@ void ContentService::Update() { auto asset = LoadedAssetsToInvoke.Dequeue(); asset->onLoaded_MainThread(); +#if USE_EDITOR + Content::onAddDependencies(asset); +#endif } LoadedAssetsToInvokeLocker.Unlock(); } @@ -1091,10 +1097,17 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat FileSystem::DeleteFile(tmpPath); // Reload storage - if (auto storage = ContentStorageManager::GetStorage(dstPath, false)) + auto storage = ContentStorageManager::GetStorage(dstPath, false); + if (storage && storage->IsLoaded()) { storage->Reload(); } + else if (auto dependencies = PendingDependencies.TryGet(dstId)) + { + // Destination storage is not loaded but there are other assets that depend on it so update them + for (const auto& e : *dependencies) + e.Item->OnDependencyModified(nullptr); + } } } else @@ -1311,6 +1324,9 @@ void Content::tryCallOnLoaded(Asset* asset) { LoadedAssetsToInvoke.RemoveAtKeepOrder(index); asset->onLoaded_MainThread(); +#if USE_EDITOR + onAddDependencies(asset); +#endif } } @@ -1328,6 +1344,10 @@ void Content::onAssetUnload(Asset* asset) Assets.Remove(asset->GetID()); UnloadQueue.Remove(asset); LoadedAssetsToInvoke.Remove(asset); +#if USE_EDITOR + for (auto& e : PendingDependencies) + e.Value.Remove(asset); +#endif } void Content::onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId) @@ -1335,8 +1355,42 @@ void Content::onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId ScopeLock locker(AssetsLocker); Assets.Remove(oldId); Assets.Add(newId, asset); +#if USE_EDITOR + if (PendingDependencies.ContainsKey(oldId)) + { + auto deps = MoveTemp(PendingDependencies[oldId]); + PendingDependencies.Remove(oldId); + PendingDependencies.Add(newId, MoveTemp(deps)); + } +#endif } +#if USE_EDITOR + +void Content::onAssetDepend(BinaryAsset* asset, const Guid& otherId) +{ + ScopeLock locker(AssetsLocker); + PendingDependencies[otherId].Add(asset); +} + +void Content::onAddDependencies(Asset* asset) +{ + auto it = PendingDependencies.Find(asset->GetID()); + if (it.IsNotEnd()) + { + auto& dependencies = it->Value; + auto binaryAsset = Asset::Cast(asset); + if (binaryAsset) + { + for (const auto& e : dependencies) + binaryAsset->_dependantAssets.Add(e.Item); + } + PendingDependencies.Remove(it); + } +} + +#endif + bool Content::IsAssetTypeIdInvalid(const ScriptingTypeHandle& type, const ScriptingTypeHandle& assetType) { // Skip if no restrictions for the type diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index 107b51270..50886c04b 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -12,6 +12,7 @@ class Engine; class FlaxFile; +class BinaryAsset; class IAssetFactory; class AssetsCache; @@ -395,6 +396,12 @@ private: static void onAssetLoaded(Asset* asset); static void onAssetUnload(Asset* asset); static void onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId); +#if USE_EDITOR + friend BinaryAsset; + friend class ContentService; + static void onAssetDepend(BinaryAsset* asset, const Guid& otherId); + static void onAddDependencies(Asset* asset); +#endif static void deleteFileSafety(const StringView& path, const Guid* id = nullptr); // Internal bindings diff --git a/Source/Engine/Content/Factories/BinaryAssetFactory.cpp b/Source/Engine/Content/Factories/BinaryAssetFactory.cpp index c492bd5b0..9486c6478 100644 --- a/Source/Engine/Content/Factories/BinaryAssetFactory.cpp +++ b/Source/Engine/Content/Factories/BinaryAssetFactory.cpp @@ -28,7 +28,7 @@ bool BinaryAssetFactoryBase::Init(BinaryAsset* asset) #if USE_EDITOR // Check if need to perform data conversion to the newer version (only in Editor) const auto upgrader = GetUpgrader(); - if (storage->AllowDataModifications() && upgrader && upgrader->ShouldUpgrade(initData.SerializedVersion)) + if (!storage->IsReadOnly() && upgrader && upgrader->ShouldUpgrade(initData.SerializedVersion)) { const auto startTime = DateTime::NowUTC(); const AssetInfo info(asset->GetID(), asset->GetTypeName(), storage->GetPath()); diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h index 4eae2829b..e7d4077fd 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h @@ -80,7 +80,6 @@ private: auto asset = Asset.Get(); if (asset) { - asset->Locker.Lock(); Task* task = (Task*)Platform::AtomicRead(&asset->_loadingTask); if (task) { @@ -99,7 +98,6 @@ private: task = task->GetContinueWithTask(); } while (task); } - asset->Locker.Unlock(); } } }; diff --git a/Source/Engine/Content/Storage/FlaxFile.h b/Source/Engine/Content/Storage/FlaxFile.h index 1fefd5ae4..387b1b43f 100644 --- a/Source/Engine/Content/Storage/FlaxFile.h +++ b/Source/Engine/Content/Storage/FlaxFile.h @@ -23,7 +23,7 @@ public: // [FlaxStorage] String ToString() const override; bool IsPackage() const override; - bool AllowDataModifications() const override; + bool IsReadOnly() const override; bool HasAsset(const Guid& id) const override; bool HasAsset(const AssetInfo& info) const override; int32 GetEntriesCount() const override; diff --git a/Source/Engine/Content/Storage/FlaxPackage.h b/Source/Engine/Content/Storage/FlaxPackage.h index dee505b74..615a2e8e4 100644 --- a/Source/Engine/Content/Storage/FlaxPackage.h +++ b/Source/Engine/Content/Storage/FlaxPackage.h @@ -24,7 +24,7 @@ public: // [FlaxStorage] String ToString() const override; bool IsPackage() const override; - bool AllowDataModifications() const override; + bool IsReadOnly() const override; bool HasAsset(const Guid& id) const override; bool HasAsset(const AssetInfo& info) const override; int32 GetEntriesCount() const override; diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index 0cdaa6a6e..8dee911e4 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -811,7 +811,7 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId) // TODO: validate entry ASSERT(newId.IsValid()); - ASSERT(AllowDataModifications()); + ASSERT(!IsReadOnly()); LOG(Info, "Changing asset \'{0}\' id to \'{1}\' (storage: \'{2}\')", e.ID, newId, _path); @@ -885,7 +885,7 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId) FlaxChunk* FlaxStorage::AllocateChunk() { - if (AllowDataModifications()) + if (!IsReadOnly()) { PROFILE_MEM(ContentFiles); auto chunk = New(); @@ -1135,7 +1135,7 @@ bool FlaxStorage::Create(WriteStream* stream, Span assets, const bool FlaxStorage::Save(const AssetInitData& data, bool silentMode) { // Check if can modify the storage - if (!AllowDataModifications()) + if (IsReadOnly()) return true; // Note: we support saving only single asset, to save more assets in single package use FlaxStorage::Create(..) @@ -1546,7 +1546,7 @@ void FlaxStorage::Tick(double time) void FlaxStorage::OnRename(const StringView& newPath) { - ASSERT(AllowDataModifications()); + ASSERT(!IsReadOnly()); _path = newPath; } @@ -1568,9 +1568,9 @@ bool FlaxFile::IsPackage() const return false; } -bool FlaxFile::AllowDataModifications() const +bool FlaxFile::IsReadOnly() const { - return true; + return false; } bool FlaxFile::HasAsset(const Guid& id) const @@ -1648,9 +1648,9 @@ bool FlaxPackage::IsPackage() const return true; } -bool FlaxPackage::AllowDataModifications() const +bool FlaxPackage::IsReadOnly() const { - return false; + return true; } bool FlaxPackage::HasAsset(const Guid& id) const diff --git a/Source/Engine/Content/Storage/FlaxStorage.h b/Source/Engine/Content/Storage/FlaxStorage.h index 6acc91981..7ac3b3b43 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.h +++ b/Source/Engine/Content/Storage/FlaxStorage.h @@ -276,9 +276,9 @@ public: virtual bool IsPackage() const = 0; /// - /// Checks whenever storage container allows the data modifications. + /// Checks whenever storage container doesn't allow modifications. /// - virtual bool AllowDataModifications() const = 0; + virtual bool IsReadOnly() const = 0; /// /// Determines whether the specified asset exists in this container. diff --git a/Source/Engine/Core/Math/Rectangle.cs b/Source/Engine/Core/Math/Rectangle.cs index 8e3c2b6c4..fcded50ce 100644 --- a/Source/Engine/Core/Math/Rectangle.cs +++ b/Source/Engine/Core/Math/Rectangle.cs @@ -253,6 +253,16 @@ namespace FlaxEngine return new Rectangle(Location + new Float2(x, y), Size); } + /// + /// Make offseted rectangle + /// + /// Offset (will be applied to X- and Y- axis). + /// Offseted rectangle. + public Rectangle MakeOffsetted(float offset) + { + return new Rectangle(Location + offset, Size); + } + /// /// Make offseted rectangle /// diff --git a/Source/Engine/Core/Types/TimeSpan.h b/Source/Engine/Core/Types/TimeSpan.h index 7704007a5..7455b7582 100644 --- a/Source/Engine/Core/Types/TimeSpan.h +++ b/Source/Engine/Core/Types/TimeSpan.h @@ -253,7 +253,7 @@ public: /// FORCE_INLINE float GetTotalSeconds() const { - return static_cast(Ticks) / TicksPerSecond; + return (float)((double)Ticks / TicksPerSecond); } public: diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 285b9ec72..aecc292dc 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -124,7 +124,7 @@ VariantType::VariantType(Types type, const StringAnsiView& typeName, bool static VariantType::VariantType(Types type, const ScriptingType& sType) : VariantType(type) { - SetTypeName(sType); + SetTypeName(sType.Fullname, sType.Module->CanReload); } VariantType::VariantType(Types type, const MClass* klass) @@ -171,7 +171,7 @@ VariantType::VariantType(const StringAnsiView& typeName) // Check case for array if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive)) { - new(this) VariantType(Array, StringAnsiView(typeName.Get(), typeName.Length() - 2)); + new(this) VariantType(Array, typeName); return; } diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 7b60907f7..13b928171 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -68,10 +68,12 @@ API_STRUCT(InBuild) struct FLAXENGINE_API VariantType MAX, #if USE_LARGE_WORLDS + Real = Double, Vector2 = Double2, Vector3 = Double3, Vector4 = Double4, #else + Real = Float, Vector2 = Float2, Vector3 = Float3, Vector4 = Float4, diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 7af32fc9b..33e102089 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1197,11 +1197,17 @@ namespace FlaxEngine.Interop } [UnmanagedCallersOnly] - internal static byte GetMethodParameterIsOut(ManagedHandle methodHandle, int parameterNum) + internal static UInt64 GetMethodParameterIsOut(ManagedHandle methodHandle) { MethodHolder methodHolder = Unsafe.As(methodHandle.Target); - ParameterInfo parameterInfo = methodHolder.method.GetParameters()[parameterNum]; - return (byte)(parameterInfo.IsOut ? 1 : 0); + var parameters = methodHolder.method.GetParameters(); + UInt64 result = 0; + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].IsOut) + result |= 1ul << i; + } + return result; } [UnmanagedCallersOnly] diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 459f1b662..1fe9e39a9 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -198,7 +198,7 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCal sphere.Center -= context.ViewOrigin; if (Float3::Distance(context.LodView.Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance && context.RenderContext.View.CullingFrustum.Intersects(sphere) && - RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq) + RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, (float)sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq) { const auto modelFrame = instance.DrawState.PrevFrame + 1; diff --git a/Source/Engine/Graphics/GPUBufferDescription.cs b/Source/Engine/Graphics/GPUBufferDescription.cs index 107d17b3e..43fcf94bf 100644 --- a/Source/Engine/Graphics/GPUBufferDescription.cs +++ b/Source/Engine/Graphics/GPUBufferDescription.cs @@ -428,6 +428,20 @@ namespace FlaxEngine partial struct VertexElement : IEquatable { + /// + /// Creates the vertex element description. + /// + /// Element type. + /// Data format. + public VertexElement(Types type, PixelFormat format) + { + Type = type; + Slot = 0; + Offset = 0; + PerInstance = 0; + Format = format; + } + /// /// Creates the vertex element description. /// diff --git a/Source/Engine/Graphics/Materials/MaterialShader.cpp b/Source/Engine/Graphics/Materials/MaterialShader.cpp index 2c8b02acc..7a39cd991 100644 --- a/Source/Engine/Graphics/Materials/MaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/MaterialShader.cpp @@ -52,13 +52,10 @@ IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderC } IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderContext& renderContext, const ::DrawCall& drawCall, bool instanced) - : GPUContext(context) - , RenderContext(renderContext) - , DrawCall(&drawCall) - , Time(Time::Draw.UnscaledTime.GetTotalSeconds()) - , ScaledTime(Time::Draw.Time.GetTotalSeconds()) - , Instanced(instanced) + : BindParameters(context, renderContext) { + DrawCall = &drawCall; + Instanced = instanced; } GPUConstantBuffer* IMaterial::BindParameters::PerViewConstants = nullptr; diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index ffe509476..cd216cdf5 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace FlaxEngine @@ -17,13 +18,14 @@ namespace FlaxEngine { private Span _data; private PixelFormat _format; - private int _stride; + private int _stride, _count; private readonly PixelFormatSampler _sampler; - internal Stream(Span data, PixelFormat format, int stride) + internal Stream(Span data, PixelFormat format, int stride, int count) { _data = data; _stride = stride; + _count = count; if (PixelFormatSampler.Get(format, out _sampler)) { _format = format; @@ -52,7 +54,7 @@ namespace FlaxEngine /// /// Gets the count of the items in the stride. /// - public int Count => _data.Length / _stride; + public int Count => _count; /// /// Returns true if stream is valid. @@ -76,6 +78,10 @@ namespace FlaxEngine /// Loaded value. public int GetInt(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return (int)_sampler.Read(data + index * _stride).X; } @@ -87,6 +93,10 @@ namespace FlaxEngine /// Loaded value. public float GetFloat(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return _sampler.Read(data + index * _stride).X; } @@ -98,6 +108,10 @@ namespace FlaxEngine /// Loaded value. public Float2 GetFloat2(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return new Float2(_sampler.Read(data + index * _stride)); } @@ -109,6 +123,10 @@ namespace FlaxEngine /// Loaded value. public Float3 GetFloat3(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return new Float3(_sampler.Read(data + index * _stride)); } @@ -120,6 +138,10 @@ namespace FlaxEngine /// Loaded value. public Float4 GetFloat4(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return _sampler.Read(data + index * _stride); } @@ -131,9 +153,13 @@ namespace FlaxEngine /// Value to assign. public void SetInt(int index, int value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -143,9 +169,13 @@ namespace FlaxEngine /// Value to assign. public void SetFloat(int index, float value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -155,9 +185,13 @@ namespace FlaxEngine /// Value to assign. public void SetFloat2(int index, Float2 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value, 0.0f, 0.0f); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -167,9 +201,13 @@ namespace FlaxEngine /// Value to assign. public void SetFloat3(int index, Float3 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value, 0.0f); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -179,8 +217,12 @@ namespace FlaxEngine /// Value to assign. public void SetFloat4(int index, Float4 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref value); + _sampler.Write(data + index * _stride, &value); } /// @@ -190,6 +232,10 @@ namespace FlaxEngine /// Pointer to the source data. public void SetLinear(IntPtr data) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif new Span(data.ToPointer(), _data.Length).CopyTo(_data); } @@ -199,6 +245,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -211,7 +261,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i], 0.0f, 0.0f); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -223,6 +273,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -235,7 +289,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i], 0.0f); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -247,6 +301,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32A32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -259,7 +317,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = (Float4)src[i]; - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -271,6 +329,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32_UInt)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -292,7 +354,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i]); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -304,6 +366,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -325,6 +391,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -346,6 +416,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32A32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -367,6 +441,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32_UInt)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -390,6 +468,17 @@ namespace FlaxEngine } } } + + /// + /// Checks if stream is valid. + /// + /// The stream to check. + /// True if stream is valid, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(Stream stream) + { + return stream.IsValid; + } } private byte[][] _data = new byte[(int)MeshBufferType.MAX][]; @@ -510,19 +599,19 @@ namespace FlaxEngine bool use16BitIndexBuffer = false; IntPtr[] vbData = new IntPtr[3]; GPUVertexLayout[] vbLayout = new GPUVertexLayout[3]; - if (_data[VB0] != null) + if (_data[VB0] != null && _data[VB0].Length != 0) { vbData[0] = dataPtr[VB0]; vbLayout[0] = _layouts[VB0]; vertices = (uint)_data[VB0].Length / _layouts[VB0].Stride; } - if (_data[VB1] != null) + if (_data[VB1] != null && _data[VB1].Length != 0) { vbData[1] = dataPtr[VB1]; vbLayout[1] = _layouts[VB1]; vertices = (uint)_data[VB1].Length / _layouts[VB1].Stride; } - if (_data[VB2] != null) + if (_data[VB2] != null && _data[VB2].Length != 0) { vbData[2] = dataPtr[VB2]; vbLayout[2] = _layouts[VB2]; @@ -576,15 +665,16 @@ namespace FlaxEngine { Span data = new Span(); PixelFormat format = PixelFormat.Unknown; - int stride = 0; + int stride = 0, count = 0; var ib = _data[(int)MeshBufferType.Index]; if (ib != null) { data = ib; format = _formats[(int)MeshBufferType.Index]; stride = PixelFormatExtensions.SizeInBytes(format); + count = data.Length / stride; } - return new Stream(data, format, stride); + return new Stream(data, format, stride, count); } /// @@ -596,7 +686,7 @@ namespace FlaxEngine { Span data = new Span(); PixelFormat format = PixelFormat.Unknown; - int stride = 0; + int stride = 0, count = 0; for (int vbIndex = 0; vbIndex < 3 && format == PixelFormat.Unknown; vbIndex++) { int idx = vbIndex + 1; @@ -611,11 +701,12 @@ namespace FlaxEngine data = new Span(vb).Slice(e.Offset); format = e.Format; stride = (int)layout.Stride; + count = vb.Length / stride; break; } } } - return new Stream(data, format, stride); + return new Stream(data, format, stride, count); } /// @@ -722,6 +813,16 @@ namespace FlaxEngine set => SetStreamFloat3(VertexElement.Types.Normal, value, PackNormal); } + /// + /// Gets or sets the vertex tangent vectors (unpacked, normalized). Null if does not exist in vertex buffers of the mesh. + /// + /// Uses stream to read or write data to the vertex buffer. + public Float3[] Tangents + { + get => GetStreamFloat3(VertexElement.Types.Tangent, UnpackNormal); + set => SetStreamFloat3(VertexElement.Types.Tangent, value, PackNormal); + } + /// /// Gets or sets the vertex UVs (texcoord channel 0). Null if does not exist in vertex buffers of the mesh. /// @@ -732,6 +833,70 @@ namespace FlaxEngine set => SetStreamFloat2(VertexElement.Types.TexCoord, value); } + /// + /// Recalculates normal vectors for all vertices. + /// + public void ComputeNormals() + { + var positions = Position(); + var indices = Index(); + if (!positions) + throw new Exception("Cannot compute tangents without positions."); + if (!indices) + throw new Exception("Cannot compute tangents without indices."); + if (!Normal()) + throw new Exception("Cannot compute tangents without Normal vertex element."); + var vertexCount = positions.Count; + if (vertexCount == 0) + return; + var indexCount = indices.Count; + + // Compute per-face normals but store them per-vertex + var normals = new Float3[vertexCount]; + for (int i = 0; i < indexCount; i += 3) + { + var i1 = indices.GetInt(i + 0); + var i2 = indices.GetInt(i + 1); + var i3 = indices.GetInt(i + 2); + Float3 v1 = positions.GetFloat3(i1); + Float3 v2 = positions.GetFloat3(i2); + Float3 v3 = positions.GetFloat3(i3); + Float3 n = Float3.Cross((v2 - v1), (v3 - v1)).Normalized; + + normals[i1] += n; + normals[i2] += n; + normals[i3] += n; + } + + // Average normals + for (int i = 0; i < normals.Length; i++) + normals[i] = normals[i].Normalized; + + // Write back to the buffer + Normals = normals; + } + + /// + /// Recalculates tangent vectors for all vertices based on normals. + /// + public void ComputeTangents() + { + var normals = Normal(); + var tangents = Tangent(); + if (!normals) + throw new Exception("Cannot compute tangents without normals."); + if (!tangents) + throw new Exception("Cannot compute tangents without Tangent vertex element."); + var count = normals.Count; + for (int i = 0; i < count; i++) + { + Float3 normal = normals.GetFloat3(i); + UnpackNormal(ref normal); + RenderTools.CalculateTangentFrame(out var n, out var t, ref normal); + tangents.SetFloat4(i, t); + } + } + private uint[] GetStreamUInt(Stream stream) { uint[] result = null; diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h index 595f76c6b..f0b40b623 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.h +++ b/Source/Engine/Graphics/Models/MeshAccessor.h @@ -25,10 +25,10 @@ public: private: Span _data; PixelFormat _format; - int32 _stride; + int32 _stride, _count; PixelFormatSampler _sampler; - Stream(Span data, PixelFormat format, int32 stride); + Stream(Span data, PixelFormat format, int32 stride, int32 count); public: Span GetData() const; @@ -38,6 +38,11 @@ public: bool IsValid() const; bool IsLinear(PixelFormat expectedFormat) const; + FORCE_INLINE operator bool() const + { + return IsValid(); + } + FORCE_INLINE int32 GetInt(int32 index) const { ASSERT_LOW_LAYER(index * _stride < _data.Length()); diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index 81fffb3e7..435f6d0c8 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -38,10 +38,11 @@ namespace #endif } -MeshAccessor::Stream::Stream(Span data, PixelFormat format, int32 stride) +MeshAccessor::Stream::Stream(Span data, PixelFormat format, int32 stride, int32 count) : _data(data) , _format(PixelFormat::Unknown) , _stride(stride) + , _count(count) { auto sampler = PixelFormatSampler::Get(format); if (sampler) @@ -72,7 +73,7 @@ int32 MeshAccessor::Stream::GetStride() const int32 MeshAccessor::Stream::GetCount() const { - return _data.Length() / _stride; + return _count; } bool MeshAccessor::Stream::IsValid() const @@ -368,22 +369,23 @@ MeshAccessor::Stream MeshAccessor::Index() { Span data; PixelFormat format = PixelFormat::Unknown; - int32 stride = 0; + int32 stride = 0, count = 0; auto& ib = _data[(int32)MeshBufferType::Index]; if (ib.IsValid()) { data = ib; format = _formats[(int32)MeshBufferType::Index]; stride = PixelFormatExtensions::SizeInBytes(format); + count = data.Length() / stride; } - return Stream(data, format, stride); + return Stream(data, format, stride, count); } MeshAccessor::Stream MeshAccessor::Attribute(VertexElement::Types attribute) { Span data; PixelFormat format = PixelFormat::Unknown; - int32 stride = 0; + int32 stride = 0, count = 0; for (int32 vbIndex = 0; vbIndex < 3 && format == PixelFormat::Unknown; vbIndex++) { static_assert((int32)MeshBufferType::Vertex0 == 1, "Update code."); @@ -399,11 +401,12 @@ MeshAccessor::Stream MeshAccessor::Attribute(VertexElement::Types attribute) data = vb.Slice(e.Offset); format = e.Format; stride = layout->GetStride(); + count = vb.Length() / stride; break; } } } - return Stream(data, format, stride); + return Stream(data, format, stride, count); } MeshBase::~MeshBase() diff --git a/Source/Engine/Graphics/PixelFormatSampler.cs b/Source/Engine/Graphics/PixelFormatSampler.cs index 8ff5a32fb..62046b2a5 100644 --- a/Source/Engine/Graphics/PixelFormatSampler.cs +++ b/Source/Engine/Graphics/PixelFormatSampler.cs @@ -22,7 +22,7 @@ namespace FlaxEngine /// /// Write data function. /// - public delegate* unmanaged Write; + public delegate* unmanaged Write; /// /// Tries to get a sampler tool for the specified format to read pixels. @@ -38,7 +38,7 @@ namespace FlaxEngine Format = format, PixelSize = pixelSize, Read = (delegate* unmanaged)read.ToPointer(), - Write = (delegate* unmanaged)write.ToPointer(), + Write = (delegate* unmanaged)write.ToPointer(), }; return pixelSize != 0; } diff --git a/Source/Engine/Graphics/RenderTools.cpp b/Source/Engine/Graphics/RenderTools.cpp index 94226d4b4..0a98f1401 100644 --- a/Source/Engine/Graphics/RenderTools.cpp +++ b/Source/Engine/Graphics/RenderTools.cpp @@ -575,6 +575,13 @@ void RenderTools::ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascad } } +bool RenderTools::ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame, bool updateForce) +{ + int32 updateFrequency, updatePhrase; + ComputeCascadeUpdateFrequency(cascadeIndex, cascadeCount, updateFrequency, updatePhrase, updateMaxCountPerFrame); + return (frameIndex % updateFrequency == updatePhrase) || updateForce; +} + float RenderTools::ComputeTemporalTime() { const float time = Time::Draw.UnscaledTime.GetTotalSeconds(); diff --git a/Source/Engine/Graphics/RenderTools.h b/Source/Engine/Graphics/RenderTools.h index 92790c288..7be73c912 100644 --- a/Source/Engine/Graphics/RenderTools.h +++ b/Source/Engine/Graphics/RenderTools.h @@ -133,12 +133,7 @@ public: static void ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascadeCount, int32& updateFrequency, int32& updatePhrase, int32 updateMaxCountPerFrame = 1); // Checks if cached data should be updated during the given frame. - FORCE_INLINE static bool ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame = 1, bool updateForce = false) - { - int32 updateFrequency, updatePhrase; - ComputeCascadeUpdateFrequency(cascadeIndex, cascadeCount, updateFrequency, updatePhrase, updateMaxCountPerFrame); - return (frameIndex % updateFrequency == updatePhrase) || updateForce; - } + static bool ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame = 1, bool updateForce = false); // Calculates temporal offset in the dithering factor that gets cleaned out by TAA. // Returns 0-1 value based on unscaled draw time for temporal effects to reduce artifacts from screen-space dithering when using Temporal Anti-Aliasing. @@ -150,8 +145,9 @@ public: DEPRECATED("Use CalculateTangentFrame with unpacked Float3/Float4.") static void CalculateTangentFrame(FloatR10G10B10A2& resultNormal, FloatR10G10B10A2& resultTangent, const Float3& normal, const Float3& tangent); // Result normal/tangent are already packed into [0;1] range. - static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal); - static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal, const Float3& tangent); + API_FUNCTION() static void CalculateTangentFrame(API_PARAM(Out) Float3& resultNormal, API_PARAM(Out) Float4& resultTangent, API_PARAM(Ref) const Float3& normal); + // Result normal/tangent are already packed into [0;1] range. + API_FUNCTION() static void CalculateTangentFrame(API_PARAM(Out) Float3& resultNormal, API_PARAM(Out) Float4& resultTangent, API_PARAM(Ref) const Float3& normal, API_PARAM(Ref) const Float3& tangent); static void ComputeSphereModelDrawMatrix(const RenderView& view, const Float3& position, float radius, Matrix& resultWorld, bool& resultIsViewInside); static void ComputeBoxModelDrawMatrix(const RenderView& view, const OrientedBoundingBox& box, Matrix& resultWorld, bool& resultIsViewInside); diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 8123142aa..3f1ab905b 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -20,12 +20,13 @@ #include "Engine/Core/Math/Double4x4.h" #include "Engine/Debug/Exceptions/JsonParseException.h" #include "Engine/Graphics/RenderTask.h" -#include "Engine/Graphics/RenderView.h" #include "Engine/Physics/Physics.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Serialization/ISerializeModifier.h" #include "Engine/Serialization/Serialization.h" +#include "Engine/Serialization/JsonTools.h" #include "Engine/Serialization/JsonWriters.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Serialization/MemoryWriteStream.h" @@ -70,6 +71,44 @@ namespace } return result; } + + void LinkPrefab(SceneObject* object, Prefab* linkPrefab) + { + // Find prefab object that is used by this object in a given prefab + Guid currentPrefabId = object->GetPrefabID(), currentObjectId = object->GetPrefabObjectID(); + RETRY: + auto prefab = Content::Load(currentPrefabId); + Guid nestedPrefabId, nestedObjectId; + if (prefab && prefab->GetNestedObject(currentObjectId, nestedPrefabId, nestedObjectId)) + { + auto nestedPrefab = Content::Load(nestedPrefabId); + if (nestedPrefab) + { + auto nestedObject = (Actor*)nestedPrefab->GetDefaultInstance(nestedObjectId); + if (nestedObject && nestedPrefab == linkPrefab) + { + object->LinkPrefab(nestedPrefabId, nestedObjectId); + return; + } + } + + // Try deeper + currentPrefabId = nestedPrefabId; + currentObjectId = nestedObjectId; + goto RETRY; + } + + // Failed to resolve properly object from a given prefab + object->BreakPrefabLink(); + } + + void LinkPrefabRecursive(Actor* actor, Prefab* linkPrefab) + { + for (auto script : actor->Scripts) + LinkPrefab(script, linkPrefab); + for (auto child : actor->Children) + LinkPrefab(child, linkPrefab); + } } Actor::Actor(const SpawnParams& params) @@ -1185,11 +1224,15 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) } else if (!parent && parentId.IsValid()) { + // Skip warning if object was mapped to empty id (intentionally ignored) Guid tmpId; - if (_prefabObjectID.IsValid()) - LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID); - else if (!modifier->IdsMapping.TryGet(parentId, tmpId) || tmpId.IsValid()) // Skip warning if object was mapped to empty id (intentionally ignored) - LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); + if (!modifier->IdsMapping.TryGet(parentId, tmpId) || tmpId.IsValid()) + { + if (_prefabObjectID.IsValid()) + LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID); + else + LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); + } } } } @@ -1729,7 +1772,6 @@ void WriteObjectToBytes(SceneObject* obj, rapidjson_flax::StringBuffer& buffer, { // Create JSON CompactJsonWriter writer(buffer); - writer.SceneObject(obj); // Write json to output @@ -1737,28 +1779,24 @@ void WriteObjectToBytes(SceneObject* obj, rapidjson_flax::StringBuffer& buffer, output.WriteInt32((int32)buffer.GetSize()); output.WriteBytes((byte*)buffer.GetString(), (int32)buffer.GetSize()); - // Store order in parent. Makes life easier for editor to sync objects order on undo/redo actions. - output.WriteInt32(obj->GetOrderInParent()); - // Reuse string buffer buffer.Clear(); } bool Actor::ToBytes(const Array& actors, MemoryWriteStream& output) { - PROFILE_CPU(); if (actors.IsEmpty()) { // Cannot serialize empty list return true; } + PROFILE_CPU(); + PROFILE_MEM(Level); // Collect object ids that exist in the serialized data to allow references mapping later Array ids(actors.Count()); for (int32 i = 0; i < actors.Count(); i++) { - // By default we collect actors and scripts (they are ManagedObjects recognized by the id) - auto actor = actors[i]; if (!actor) continue; @@ -1766,7 +1804,8 @@ bool Actor::ToBytes(const Array& actors, MemoryWriteStream& output) for (int32 j = 0; j < actor->Scripts.Count(); j++) { const auto script = actor->Scripts[j]; - ids.Add(script->GetID()); + if (script) + ids.Add(script->GetID()); } } @@ -1776,23 +1815,28 @@ bool Actor::ToBytes(const Array& actors, MemoryWriteStream& output) // Serialized objects ids (for references mapping) output.Write(ids); - // Objects data + // Objects data (JSON) rapidjson_flax::StringBuffer buffer; + CompactJsonWriter writer(buffer); + writer.StartArray(); for (int32 i = 0; i < actors.Count(); i++) { Actor* actor = actors[i]; if (!actor) continue; - - WriteObjectToBytes(actor, buffer, output); - + writer.SceneObject(actor); for (int32 j = 0; j < actor->Scripts.Count(); j++) { Script* script = actor->Scripts[j]; - - WriteObjectToBytes(script, buffer, output); + if (!script) + continue; + writer.SceneObject(script); } } + writer.EndArray(); + // TODO: compress json (LZ4) if it's big enough + output.WriteInt32((int32)buffer.GetSize()); + output.WriteBytes((byte*)buffer.GetString(), (int32)buffer.GetSize()); return false; } @@ -1811,8 +1855,8 @@ Array Actor::ToBytes(const Array& actors) bool Actor::FromBytes(const Span& data, Array& output, ISerializeModifier* modifier) { PROFILE_CPU(); + PROFILE_MEM(Level); output.Clear(); - ASSERT(modifier); if (data.Length() <= 0) return true; @@ -1828,50 +1872,71 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM } // Serialized objects ids (for references mapping) +#if 0 Array ids; stream.Read(ids); int32 objectsCount = ids.Count(); +#else + int32 objectsCount; + stream.ReadInt32(&objectsCount); + stream.Move(objectsCount); +#endif if (objectsCount < 0) return true; + // Load objects data (JSON) + int32 bufferSize; + stream.ReadInt32(&bufferSize); + const char* buffer = (const char*)stream.Move(bufferSize); + rapidjson_flax::Document document; + { + PROFILE_CPU_NAMED("Json.Parse"); + document.Parse(buffer, bufferSize); + } + if (document.HasParseError()) + { + Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); + return true; + } + // Prepare - Array order; - order.Resize(objectsCount); modifier->EngineBuild = engineBuild; CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); sceneObjects->Resize(objectsCount); SceneObjectsFactory::Context context(modifier); + // Fix root linkage for prefab instances (eg. when user duplicates a sub-prefab actor but not a root one) + SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, document, modifier); + SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData); + for (auto& instance : context.Instances) + { + Guid prefabObjectId; + if (!JsonTools::GetGuidIfValid(prefabObjectId, document[instance.RootIndex], "PrefabObjectID")) + continue; + + // Get the original object from prefab + SceneObject* prefabObject = instance.Prefab->GetDefaultInstance(prefabObjectId); + if (prefabObject && prefabObject->GetParent()) + { + // Add empty mapping to parent object in prefab to prevent linking to it + auto prefabObjectParentId = prefabObject->GetParent()->GetPrefabObjectID(); + instance.IdsMapping[prefabObjectParentId] = Guid::Empty; + modifier->IdsMapping[prefabObjectParentId] = Guid::Empty; + Guid nestedPrefabId, nestedPrefabObjectId; + if (instance.Prefab->GetNestedObject(prefabObjectParentId, nestedPrefabId, nestedPrefabObjectId)) + { + instance.IdsMapping[nestedPrefabObjectId] = Guid::Empty; + modifier->IdsMapping[nestedPrefabObjectId] = Guid::Empty; + } + } + } + // Deserialize objects Scripting::ObjectsLookupIdMapping.Set(&modifier->IdsMapping); - auto startPos = stream.GetPosition(); for (int32 i = 0; i < objectsCount; i++) { - // Buffer - int32 bufferSize; - stream.ReadInt32(&bufferSize); - const char* buffer = (const char*)stream.GetPositionHandle(); - stream.Move(bufferSize); - - // Order in parent - int32 orderInParent; - stream.ReadInt32(&orderInParent); - order[i] = orderInParent; - - // Load JSON - rapidjson_flax::Document document; - { - PROFILE_CPU_NAMED("Json.Parse"); - document.Parse(buffer, bufferSize); - } - if (document.HasParseError()) - { - Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); - return true; - } - - // Create object - auto obj = SceneObjectsFactory::Spawn(context, document); + auto& objData = document[i]; + auto obj = SceneObjectsFactory::Spawn(context, objData); sceneObjects->At(i) = obj; if (obj == nullptr) { @@ -1879,57 +1944,21 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM continue; } obj->RegisterObject(); - - // Add to results Actor* actor = dynamic_cast(obj); if (actor) - { output.Add(actor); - } } - // TODO: optimize this to call json parsing only once per-object instead of twice (spawn + load) - stream.SetPosition(startPos); for (int32 i = 0; i < objectsCount; i++) { - // Buffer - int32 bufferSize; - stream.ReadInt32(&bufferSize); - const char* buffer = (const char*)stream.GetPositionHandle(); - stream.Move(bufferSize); - - // Order in parent - int32 orderInParent; - stream.ReadInt32(&orderInParent); - - // Load JSON - rapidjson_flax::Document document; - { - PROFILE_CPU_NAMED("Json.Parse"); - document.Parse(buffer, bufferSize); - } - if (document.HasParseError()) - { - Log::JsonParseException(document.GetParseError(), document.GetErrorOffset()); - return true; - } - - // Deserialize object + auto& objData = document[i]; auto obj = sceneObjects->At(i); if (obj) - SceneObjectsFactory::Deserialize(context, obj, document); + SceneObjectsFactory::Deserialize(context, obj, objData); else - SceneObjectsFactory::HandleObjectDeserializationError(document); + SceneObjectsFactory::HandleObjectDeserializationError(objData); } Scripting::ObjectsLookupIdMapping.Set(nullptr); - // Update objects order - //for (int32 i = 0; i < objectsCount; i++) - { - //SceneObject* obj = sceneObjects->At(i); - // TODO: remove order from saved data? - //obj->SetOrderInParent(order[i]); - } - // Call events (only for parents because they will propagate events down the tree) CollectionPoolCache::ScopeCache parents = ActorsCache::ActorsListCache.Get(); parents->EnsureCapacity(output.Count()); @@ -1940,6 +1969,32 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM Actor* actor = parents->At(i); if (actor->HasPrefabLink() && !actor->IsPrefabRoot()) { + // Find a prefab in which that object is a root to establish a new linkage + Guid currentPrefabId = actor->GetPrefabID(), currentObjectId = actor->GetPrefabObjectID(); + RETRY: + auto prefab = Content::Load(currentPrefabId); + Guid nestedPrefabId, nestedObjectId; + if (prefab && prefab->GetNestedObject(currentObjectId, nestedPrefabId, nestedObjectId)) + { + auto nestedPrefab = Content::Load(nestedPrefabId); + if (nestedPrefab) + { + auto nestedObject = (Actor*)nestedPrefab->GetDefaultInstance(nestedObjectId); + if (nestedObject && nestedObject->IsPrefabRoot()) + { + // Change link to the nested prefab + actor->LinkPrefab(nestedPrefabId, nestedObjectId); + LinkPrefabRecursive(actor, nestedPrefab); + continue; + } + } + + // Try deeper + currentPrefabId = nestedPrefabId; + currentObjectId = nestedObjectId; + goto RETRY; + } + actor->BreakPrefabLink(); } } @@ -1949,8 +2004,7 @@ bool Actor::FromBytes(const Span& data, Array& output, ISerializeM } for (int32 i = 0; i < parents->Count(); i++) { - Actor* actor = parents->At(i); - actor->OnTransformChanged(); + parents->At(i)->OnTransformChanged(); } // Initialize actor that are spawned to scene or create managed instanced for others diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index d6c2417c8..271609396 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -507,7 +507,7 @@ namespace return; Spline::Keyframe* prev = spline->Curve.GetKeyframes().Get(); Vector3 prevPos = transform.LocalToWorld(prev->Value.Translation); - float distance = Vector3::Distance(prevPos, DebugDraw::GetViewPos()); + Real distance = Vector3::Distance(prevPos, DebugDraw::GetViewPos()); if (distance < METERS_TO_UNITS(800)) // 800m { // Bezier curve diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 378b706ed..5136a6cce 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -650,6 +650,11 @@ bool Prefab::ApplyAll(Actor* targetActor) LOG(Warning, "Failed to create default prefab instance for the prefab asset."); return true; } + if (targetActor == _defaultInstance || targetActor->HasActorInHierarchy(_defaultInstance) || _defaultInstance->HasActorInHierarchy(targetActor)) + { + LOG(Error, "Cannot apply changes to the prefab using default instance. Use manually spawned prefab instance instead."); + return true; + } if (targetActor->GetPrefabObjectID() != GetRootObjectId()) { LOG(Warning, "Applying prefab changes with modified root object. Root object id: {0}, new root: {1} (prefab object id: {2})", GetRootObjectId().ToString(), targetActor->ToString(), targetActor->GetPrefabObjectID()); diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h index 9e6023a82..cde99a2cb 100644 --- a/Source/Engine/Level/Prefabs/Prefab.h +++ b/Source/Engine/Level/Prefabs/Prefab.h @@ -60,12 +60,14 @@ public: /// /// Requests the default prefab object instance. Deserializes the prefab objects from the asset. Skips if already done. /// + /// Default instances of the prefab are read-only and are used internally for objects serialization (prefab diff). /// The root of the prefab object loaded from the prefab. Contains the default values. It's not added to gameplay but deserialized with postLoad and init event fired. API_FUNCTION() Actor* GetDefaultInstance(); /// /// Requests the default prefab object instance. Deserializes the prefab objects from the asset. Skips if already done. /// + /// Default instances of the prefab are read-only and are used internally for objects serialization (prefab diff). /// The ID of the object to get from prefab default object. It can be one of the child-actors or any script that exists in the prefab. Methods returns root if id is empty. /// The object of the prefab loaded from the prefab. Contains the default values. It's not added to gameplay but deserialized with postLoad and init event fired. API_FUNCTION() SceneObject* GetDefaultInstance(API_PARAM(Ref) const Guid& objectId); diff --git a/Source/Engine/Level/SceneObject.cpp b/Source/Engine/Level/SceneObject.cpp index 1a447af15..f6a14ae5d 100644 --- a/Source/Engine/Level/SceneObject.cpp +++ b/Source/Engine/Level/SceneObject.cpp @@ -61,7 +61,7 @@ void SceneObject::BreakPrefabLink() String SceneObject::GetNamePath(Char separatorChar) const { - Array names; + Array> names; const Actor* a = dynamic_cast(this); if (!a) a = GetParent(); diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index ced964436..2ee14508a 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1863,7 +1863,7 @@ void NetworkInternal::NetworkReplicatorClientConnected(NetworkClient* client) { auto& item = it->Item; ScriptingObject* obj = item.Object.Get(); - if (!obj || !item.Spawned || item.Role != NetworkObjectRole::OwnedAuthoritative) + if (!obj || item.Role != NetworkObjectRole::OwnedAuthoritative) continue; // Mark this client as missing cached data diff --git a/Source/Engine/Renderer/GBufferPass.cpp b/Source/Engine/Renderer/GBufferPass.cpp index e62970f83..6b3a655c0 100644 --- a/Source/Engine/Renderer/GBufferPass.cpp +++ b/Source/Engine/Renderer/GBufferPass.cpp @@ -425,7 +425,7 @@ void GBufferPass::DrawSky(RenderContext& renderContext, GPUContext* context) BoundingSphere frustumBounds; renderContext.View.CullingFrustum.GetSphere(frustumBounds); origin = frustumBounds.Center; - size = frustumBounds.Radius; + size = (float)frustumBounds.Radius; } Matrix::Scaling(size / ((float)box.GetSize().Y * 0.5f) * 0.95f, m1); // Scale to fit whole view frustum Matrix::CreateWorld(origin, Float3::Up, Float3::Backward, m2); // Rotate sphere model diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 9737ae509..95e4ce0dc 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -747,7 +747,7 @@ void RenderList::AddDrawCall(const RenderContextBatch& renderContextBatch, DrawP if (drawModes != DrawPass::None && (staticFlags & renderContext.View.StaticFlagsMask) == renderContext.View.StaticFlagsCompare && renderContext.View.CullingFrustum.Intersects(bounds) && - RenderTools::ComputeBoundsScreenRadiusSquared(bounds.Center, bounds.Radius, renderContext.View) * (renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y) >= minObjectPixelSizeSq) + RenderTools::ComputeBoundsScreenRadiusSquared(bounds.Center, (float)bounds.Radius, renderContext.View) * (renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y) >= minObjectPixelSizeSq) { renderContext.List->ShadowDepthDrawCallsList.Indices.Add(index); } diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index bbcd7de57..02358fac0 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -17,7 +17,6 @@ #include "FlaxEngine.Gen.h" #include "Scripting.h" #include "Events.h" -#include "Internal/StdTypesContainer.h" Dictionary, void(*)(ScriptingObject*, void*, bool)> ScriptingEvents::EventsTable; Delegate, ScriptingTypeHandle, StringView> ScriptingEvents::Event; @@ -32,6 +31,18 @@ ManagedBinaryModule* GetBinaryModuleCorlib() #endif } +MMethod* MClass::FindMethod(const char* name, int32 numParams, bool checkBaseClasses) const +{ + MMethod* method = GetMethod(name, numParams); + if (!method && checkBaseClasses) + { + MClass* base = GetBaseClass(); + if (base) + method = base->FindMethod(name, numParams, true); + } + return method; +} + ScriptingTypeHandle::ScriptingTypeHandle(const ScriptingTypeInitializer& initializer) : Module(initializer.Module) , TypeIndex(initializer.TypeIndex) @@ -835,61 +846,17 @@ namespace } return nullptr; } - - bool VariantTypeEquals(const VariantType& type, MType* mType, bool isOut = false) - { - MClass* mClass = MCore::Type::GetClass(mType); - MClass* variantClass = MUtils::GetClass(type); - if (variantClass != mClass) - { - // Hack for Vector2/3/4 which alias with Float2/3/4 or Double2/3/4 (depending on USE_LARGE_WORLDS) - const auto& stdTypes = *StdTypesContainer::Instance(); - if (mClass == stdTypes.Vector2Class && (type.Type == VariantType::Float2 || type.Type == VariantType::Double2)) - return true; - if (mClass == stdTypes.Vector3Class && (type.Type == VariantType::Float3 || type.Type == VariantType::Double3)) - return true; - if (mClass == stdTypes.Vector4Class && (type.Type == VariantType::Float4 || type.Type == VariantType::Double4)) - return true; - - return false; - } - return true; - } } #endif -MMethod* ManagedBinaryModule::FindMethod(MClass* mclass, const ScriptingTypeMethodSignature& signature) +MMethod* ManagedBinaryModule::FindMethod(const MClass* mclass, const ScriptingTypeMethodSignature& signature) { #if USE_CSHARP - if (!mclass) - return nullptr; - const auto& methods = mclass->GetMethods(); - for (MMethod* method : methods) - { - if (method->IsStatic() != signature.IsStatic) - continue; - if (method->GetName() != signature.Name) - continue; - if (method->GetParametersCount() != signature.Params.Count()) - continue; - bool isValid = true; - for (int32 paramIdx = 0; paramIdx < signature.Params.Count(); paramIdx++) - { - auto& param = signature.Params[paramIdx]; - MType* type = method->GetParameterType(paramIdx); - if (param.IsOut != method->GetParameterIsOut(paramIdx) || - !VariantTypeEquals(param.Type, type, param.IsOut)) - { - isValid = false; - break; - } - } - if (isValid && VariantTypeEquals(signature.ReturnType, method->GetReturnType())) - return method; - } -#endif + return mclass ? mclass->GetMethod(signature) : nullptr; +#else return nullptr; +#endif } #if USE_CSHARP diff --git a/Source/Engine/Scripting/BinaryModule.h b/Source/Engine/Scripting/BinaryModule.h index 1da35401b..db346b13c 100644 --- a/Source/Engine/Scripting/BinaryModule.h +++ b/Source/Engine/Scripting/BinaryModule.h @@ -23,7 +23,7 @@ struct ScriptingTypeMethodSignature StringAnsiView Name; VariantType ReturnType; - bool IsStatic; + bool IsStatic = false; Array> Params; }; @@ -322,7 +322,7 @@ public: #endif static ScriptingObject* ManagedObjectSpawn(const ScriptingObjectSpawnParams& params); - static MMethod* FindMethod(MClass* mclass, const ScriptingTypeMethodSignature& signature); + static MMethod* FindMethod(const MClass* mclass, const ScriptingTypeMethodSignature& signature); #if USE_CSHARP static ManagedBinaryModule* FindModule(const MClass* klass); static ScriptingTypeHandle FindType(const MClass* klass); diff --git a/Source/Engine/Scripting/ManagedCLR/MClass.h b/Source/Engine/Scripting/ManagedCLR/MClass.h index 61273dae7..ea26451c1 100644 --- a/Source/Engine/Scripting/ManagedCLR/MClass.h +++ b/Source/Engine/Scripting/ManagedCLR/MClass.h @@ -188,11 +188,11 @@ public: MClass* GetBaseClass() const; /// - /// Checks if this class is a sub class of the specified class (including any derived types). + /// Checks if this class is a subclass of the specified class (including any derived types). /// /// The class. /// True if check interfaces, otherwise just base class. - /// True if this class is a sub class of the specified class. + /// True if this class is a subclass of the specified class. bool IsSubClassOf(const MClass* klass, bool checkInterfaces = false) const; /// @@ -206,7 +206,7 @@ public: /// Checks is the provided object instance of this class' type. /// /// The object to check. - /// True if object is an instance the this class. + /// True if object is an instance this class. bool IsInstanceOfType(MObject* object) const; /// @@ -227,17 +227,7 @@ public: /// The method parameters count. /// True if check base classes when searching for the given method. /// The method or null if failed to find it. - MMethod* FindMethod(const char* name, int32 numParams, bool checkBaseClasses = true) const - { - MMethod* method = GetMethod(name, numParams); - if (!method && checkBaseClasses) - { - MClass* base = GetBaseClass(); - if (base) - method = base->FindMethod(name, numParams, true); - } - return method; - } + MMethod* FindMethod(const char* name, int32 numParams, bool checkBaseClasses = true) const; /// /// Returns an object referencing a method with the specified name and number of parameters. @@ -248,6 +238,13 @@ public: /// The method or null if failed to get it. MMethod* GetMethod(const char* name, int32 numParams = 0) const; + /// + /// Returns an object referencing a method with the specified signature. + /// + /// The method signature. + /// The method or null if failed to get it. + MMethod* GetMethod(const struct ScriptingTypeMethodSignature& signature) const; + /// /// Returns all methods belonging to this class. /// @@ -271,7 +268,7 @@ public: const Array& GetFields() const; /// - /// Returns an object referencing a event with the specified name. + /// Returns an object referencing an event with the specified name. /// /// The event name. /// The event object. diff --git a/Source/Engine/Scripting/ManagedCLR/MMethod.h b/Source/Engine/Scripting/ManagedCLR/MMethod.h index 51264d1b8..f6e6b37e2 100644 --- a/Source/Engine/Scripting/ManagedCLR/MMethod.h +++ b/Source/Engine/Scripting/ManagedCLR/MMethod.h @@ -29,6 +29,7 @@ protected: int32 _paramsCount; mutable void* _returnType; mutable Array> _parameterTypes; + mutable uint64 _parameterOuts = 0; void CacheSignature() const; #else StringAnsiView _name; diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp index 69fb5aa08..0d7617e0d 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp @@ -845,7 +845,6 @@ MClass* MUtils::GetClass(const VariantType& value) auto mclass = Scripting::FindClass(StringAnsiView(value.TypeName)); if (mclass) return mclass; - const auto& stdTypes = *StdTypesContainer::Instance(); switch (value.Type) { case VariantType::Void: @@ -891,25 +890,25 @@ MClass* MUtils::GetClass(const VariantType& value) case VariantType::Double4: return Double4::TypeInitializer.GetClass(); case VariantType::Color: - return stdTypes.ColorClass; + return Color::TypeInitializer.GetClass(); case VariantType::Guid: - return stdTypes.GuidClass; + return GetBinaryModuleCorlib()->Assembly->GetClass("System.Guid"); case VariantType::Typename: - return stdTypes.TypeClass; + return GetBinaryModuleCorlib()->Assembly->GetClass("System.Type"); case VariantType::BoundingBox: - return stdTypes.BoundingBoxClass; + return BoundingBox::TypeInitializer.GetClass(); case VariantType::BoundingSphere: - return stdTypes.BoundingSphereClass; + return BoundingSphere::TypeInitializer.GetClass(); case VariantType::Quaternion: - return stdTypes.QuaternionClass; + return Quaternion::TypeInitializer.GetClass(); case VariantType::Transform: - return stdTypes.TransformClass; + return Transform::TypeInitializer.GetClass(); case VariantType::Rectangle: - return stdTypes.RectangleClass; + return Rectangle::TypeInitializer.GetClass(); case VariantType::Ray: - return stdTypes.RayClass; + return Ray::TypeInitializer.GetClass(); case VariantType::Matrix: - return stdTypes.MatrixClass; + return Matrix::TypeInitializer.GetClass(); case VariantType::Array: if (value.TypeName) { @@ -1202,8 +1201,7 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) if (value.Type.Type != VariantType::Array) return nullptr; MObject* object = BoxVariant(value); - auto typeStr = MCore::Type::ToString(type); - if (object && !MCore::Object::GetClass(object)->IsSubClassOf(MCore::Type::GetClass(type))) + if (object && MCore::Type::GetClass(type) != MCore::Array::GetArrayClass((MArray*)object)) object = nullptr; return object; } @@ -1238,6 +1236,29 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) return nullptr; } +bool MUtils::VariantTypeEquals(const VariantType& type, MType* mType, bool isOut) +{ + MClass* mClass = MCore::Type::GetClass(mType); + MClass* variantClass = MUtils::GetClass(type); + if (variantClass != mClass) + { + // Hack for Vector2/3/4 which alias with Float2/3/4 or Double2/3/4 (depending on USE_LARGE_WORLDS) + if (mClass->GetFullName() == StringAnsiView("FlaxEngine.Vector2", 18) && (type.Type == VariantType::Float2 || type.Type == VariantType::Double2)) + return true; + if (mClass->GetFullName() == StringAnsiView("FlaxEngine.Vector3", 18) && (type.Type == VariantType::Float3 || type.Type == VariantType::Double3)) + return true; + if (mClass->GetFullName() == StringAnsiView("FlaxEngine.Vector4", 18) && (type.Type == VariantType::Float4 || type.Type == VariantType::Double4)) + return true; + + // Arrays + if (type == VariantType::Array && type.GetElementType() == VariantType::Object) + return MCore::Type::GetType(mType) == MTypes::Array; + + return false; + } + return true; +} + MObject* MUtils::ToManaged(const Version& value) { #if USE_NETCORE diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.h b/Source/Engine/Scripting/ManagedCLR/MUtils.h index f642681d2..5598dbee0 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.h +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.h @@ -621,6 +621,7 @@ namespace MUtils #endif extern void* VariantToManagedArgPtr(Variant& value, MType* type, bool& failed); + extern bool VariantTypeEquals(const VariantType& type, MType* mType, bool isOut = false); extern MObject* ToManaged(const Version& value); extern Version ToNative(MObject* value); diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 4be0ce1a1..65dd09b44 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -1062,10 +1062,39 @@ MClass* MClass::GetElementClass() const MMethod* MClass::GetMethod(const char* name, int32 numParams) const { GetMethods(); - for (int32 i = 0; i < _methods.Count(); i++) + for (MMethod* method : _methods) { - if (_methods[i]->GetParametersCount() == numParams && _methods[i]->GetName() == name) - return _methods[i]; + if (method->GetParametersCount() == numParams && method->GetName() == name) + return method; + } + return nullptr; +} + +MMethod* MClass::GetMethod(const ScriptingTypeMethodSignature& signature) const +{ + GetMethods(); + for (MMethod* method : _methods) + { + if (method->IsStatic() != signature.IsStatic) + continue; + if (method->GetName() != signature.Name) + continue; + if (method->GetParametersCount() != signature.Params.Count()) + continue; + bool isValid = true; + for (int32 paramIdx = 0; paramIdx < signature.Params.Count(); paramIdx++) + { + auto& param = signature.Params[paramIdx]; + MType* type = method->GetParameterType(paramIdx); + if (param.IsOut != method->GetParameterIsOut(paramIdx) || + !MUtils::VariantTypeEquals(param.Type, type, param.IsOut)) + { + isValid = false; + break; + } + } + if (isValid && (signature.ReturnType.Type == VariantType::Null || MUtils::VariantTypeEquals(signature.ReturnType, method->GetReturnType()))) + return method; } return nullptr; } @@ -1485,13 +1514,16 @@ void MMethod::CacheSignature() const static void* GetMethodReturnTypePtr = GetStaticMethodPointer(TEXT("GetMethodReturnType")); static void* GetMethodParameterTypesPtr = GetStaticMethodPointer(TEXT("GetMethodParameterTypes")); + static void* GetMethodParameterIsOutPtr = GetStaticMethodPointer(TEXT("GetMethodParameterIsOut")); _returnType = CallStaticMethod(GetMethodReturnTypePtr, _handle); + _parameterOuts = 0; if (_paramsCount != 0) { void** parameterTypeHandles; CallStaticMethod(GetMethodParameterTypesPtr, _handle, ¶meterTypeHandles); _parameterTypes.Set(parameterTypeHandles, _paramsCount); MCore::GC::FreeMemory(parameterTypeHandles); + _parameterOuts = CallStaticMethod(GetMethodParameterIsOutPtr, _handle); } _hasCachedSignature = true; @@ -1558,9 +1590,7 @@ bool MMethod::GetParameterIsOut(int32 paramIdx) const if (!_hasCachedSignature) CacheSignature(); ASSERT_LOW_LAYER(paramIdx >= 0 && paramIdx < _paramsCount); - // TODO: cache GetParameterIsOut maybe? - static void* GetMethodParameterIsOutPtr = GetStaticMethodPointer(TEXT("GetMethodParameterIsOut")); - return CallStaticMethod(GetMethodParameterIsOutPtr, _handle, paramIdx); + return _parameterOuts & (1ull << paramIdx); } bool MMethod::HasAttribute(const MClass* klass) const diff --git a/Source/Engine/Scripting/Runtime/Mono.cpp b/Source/Engine/Scripting/Runtime/Mono.cpp index 06392f932..1449d405f 100644 --- a/Source/Engine/Scripting/Runtime/Mono.cpp +++ b/Source/Engine/Scripting/Runtime/Mono.cpp @@ -1357,6 +1357,11 @@ MMethod* MClass::GetMethod(const char* name, int32 numParams) const return method; } +MMethod* MClass::GetMethod(const ScriptingTypeMethodSignature& signature) const +{ + return GetMethod(signature.Name.Get(), signature.Params.Count()); +} + const Array& MClass::GetMethods() const { if (_hasCachedMethods) diff --git a/Source/Engine/Scripting/Runtime/None.cpp b/Source/Engine/Scripting/Runtime/None.cpp index 1ddaeae8e..178e98444 100644 --- a/Source/Engine/Scripting/Runtime/None.cpp +++ b/Source/Engine/Scripting/Runtime/None.cpp @@ -363,6 +363,11 @@ MMethod* MClass::GetMethod(const char* name, int32 numParams) const return nullptr; } +MMethod* MClass::GetMethod(const ScriptingTypeMethodSignature& signature) const +{ + return nullptr; +} + const Array& MClass::GetMethods() const { _hasCachedMethods = true; diff --git a/Source/Engine/Serialization/JsonTools.cpp b/Source/Engine/Serialization/JsonTools.cpp index 1e5b8fff6..9d89fe034 100644 --- a/Source/Engine/Serialization/JsonTools.cpp +++ b/Source/Engine/Serialization/JsonTools.cpp @@ -72,6 +72,13 @@ void ChangeIds(rapidjson_flax::Value& obj, rapidjson_flax::Document& document, c } } +void JsonTools::MergeObjects(Value& target, Value& source, Value::AllocatorType& allocator) +{ + ASSERT(target.IsObject() && source.IsObject()); + for (auto itr = source.MemberBegin(); itr != source.MemberEnd(); ++itr) + target.AddMember(itr->name, itr->value, allocator); +} + void JsonTools::ChangeIds(Document& doc, const Dictionary& mapping) { if (mapping.IsEmpty()) @@ -235,6 +242,14 @@ Plane JsonTools::GetPlane(const Value& value) return result; } +Rectangle JsonTools::GetRectangle(const Value& value) +{ + return Rectangle( + GetVector2(value, "Location", Vector2::Zero), + GetVector2(value, "Size", Vector2::Zero) + ); +} + BoundingSphere JsonTools::GetBoundingSphere(const Value& value) { BoundingSphere result; @@ -283,3 +298,14 @@ DateTime JsonTools::GetDateTime(const Value& value) { return DateTime(value.GetInt64()); } + +bool JsonTools::GetGuidIfValid(Guid& result, const Value& node, const char* name) +{ + auto member = node.FindMember(name); + if (member != node.MemberEnd()) + { + result = GetGuid(member->value); + return result.IsValid(); + } + return false; +} diff --git a/Source/Engine/Serialization/JsonTools.h b/Source/Engine/Serialization/JsonTools.h index 0d5c7115d..6e744fcc2 100644 --- a/Source/Engine/Serialization/JsonTools.h +++ b/Source/Engine/Serialization/JsonTools.h @@ -35,13 +35,7 @@ public: MergeObjects(target, source, target.GetAllocator()); } - static void MergeObjects(Value& target, Value& source, Value::AllocatorType& allocator) - { - ASSERT(target.IsObject() && source.IsObject()); - for (auto itr = source.MemberBegin(); itr != source.MemberEnd(); ++itr) - target.AddMember(itr->name, itr->value, allocator); - } - + static void MergeObjects(Value& target, Value& source, Value::AllocatorType& allocator); static void ChangeIds(Document& doc, const Dictionary& mapping); public: @@ -75,11 +69,9 @@ public: static Float2 GetFloat2(const Value& value); static Float3 GetFloat3(const Value& value); static Float4 GetFloat4(const Value& value); - static Double2 GetDouble2(const Value& value); static Double3 GetDouble3(const Value& value); static Double4 GetDouble4(const Value& value); - static Color GetColor(const Value& value); static Quaternion GetQuaternion(const Value& value); static Ray GetRay(const Value& value); @@ -87,15 +79,7 @@ public: static Transform GetTransform(const Value& value); static void GetTransform(Transform& result, const Value& value); static Plane GetPlane(const Value& value); - - static Rectangle GetRectangle(const Value& value) - { - return Rectangle( - GetVector2(value, "Location", Vector2::Zero), - GetVector2(value, "Size", Vector2::Zero) - ); - } - + static Rectangle GetRectangle(const Value& value); static BoundingSphere GetBoundingSphere(const Value& value); static BoundingBox GetBoundingBox(const Value& value); static Guid GetGuid(const Value& value); @@ -176,16 +160,7 @@ public: return member != node.MemberEnd() ? GetGuid(member->value) : Guid::Empty; } - FORCE_INLINE static bool GetGuidIfValid(Guid& result, const Value& node, const char* name) - { - auto member = node.FindMember(name); - if (member != node.MemberEnd()) - { - result = GetGuid(member->value); - return result.IsValid(); - } - return false; - } + static bool GetGuidIfValid(Guid& result, const Value& node, const char* name); public: FORCE_INLINE static void GetBool(bool& result, const Value& node, const char* name) diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index cadc9821c..164fce747 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -439,7 +439,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh // Calculate normals for quad two vertices Float3 n0 = Float3::Normalize((v00 - v01) ^ (v01 - v10)); Float3 n1 = Float3::Normalize((v11 - v10) ^ (v10 - v01)); - Float3 n2 = n0 + n1; + Float3 n2 = Float3::Normalize(n0 + n1); // Apply normal to each vertex using it normalsPerVertex[i00] += n1; @@ -2572,9 +2572,9 @@ void TerrainPatch::ExtractCollisionGeometry(Array& vertexBuffer, Array& vertexBuffer, Array', '<', + '=', }; /// diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index df542aacf..4a6bb7ce1 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -61,6 +61,9 @@ namespace FlaxEngine /// public override bool CanRender() { + if (!Canvas) + return false; + // Sync with canvas options Location = Canvas.RenderLocation; Order = Canvas.Order; diff --git a/Source/Engine/Visject/ShaderGraph.cpp b/Source/Engine/Visject/ShaderGraph.cpp index b297ad91e..1df4ea439 100644 --- a/Source/Engine/Visject/ShaderGraph.cpp +++ b/Source/Engine/Visject/ShaderGraph.cpp @@ -493,35 +493,26 @@ void ShaderGenerator::ProcessGroupPacking(Box* box, Node* node, Value& value) // Unpack case 30: { - Box* b = node->GetBox(0); - Value v = tryGetValue(b, Float2::Zero).AsFloat2(); - - int32 subIndex = box->ID - 1; - ASSERT(subIndex >= 0 && subIndex < 2); - - value = Value(ValueType::Float, v.Value + _subs[subIndex]); + value = tryGetValue(node->GetBox(0), Float2::Zero).AsFloat2(); + const int32 subIndex = box->ID - 1; + if (subIndex >= 0 && subIndex < 2) + value = Value(ValueType::Float, value.Value + _subs[subIndex]); break; } case 31: { - Box* b = node->GetBox(0); - Value v = tryGetValue(b, Float3::Zero).AsFloat3(); - - int32 subIndex = box->ID - 1; - ASSERT(subIndex >= 0 && subIndex < 3); - - value = Value(ValueType::Float, v.Value + _subs[subIndex]); + value = tryGetValue(node->GetBox(0), Float3::Zero).AsFloat3(); + const int32 subIndex = box->ID - 1; + if (subIndex >= 0 && subIndex < 3) + value = Value(ValueType::Float, value.Value + _subs[subIndex]); break; } case 32: { - Box* b = node->GetBox(0); - Value v = tryGetValue(b, Float4::Zero).AsFloat4(); - - int32 subIndex = box->ID - 1; - ASSERT(subIndex >= 0 && subIndex < 4); - - value = Value(ValueType::Float, v.Value + _subs[subIndex]); + value = tryGetValue(node->GetBox(0), Float4::Zero).AsFloat4(); + const int32 subIndex = box->ID - 1; + if (subIndex >= 0 && subIndex < 4) + value = Value(ValueType::Float, value.Value + _subs[subIndex]); break; } case 33: diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 760e7a926..6dd2aba6f 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -201,13 +201,15 @@ namespace Flax.Build.Bindings if (apiType.IsStruct && !apiType.IsPod && !CppUsedNonPodTypes.Contains(apiType)) CppUsedNonPodTypes.Add(apiType); + var fullname = apiType.FullNameManaged; if (apiType.IsEnum) - return $"Variant::Enum(VariantType(VariantType::Enum, StringAnsiView(\"{apiType.FullNameManaged}\", {apiType.FullNameManaged.Length})), {value})"; + return $"Variant::Enum(VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length})), {value})"; if (apiType.IsStruct && !CppInBuildVariantStructures.Contains(apiType.Name)) - if (typeInfo.IsPtr) - return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{apiType.FullNameManaged}\", {apiType.FullNameManaged.Length})), *{value})"; - else - return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{apiType.FullNameManaged}\", {apiType.FullNameManaged.Length})), {value})"; + { + if (apiType.IsInBuild) + return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length})), {(typeInfo.IsPtr ? "*" + value : value)})"; + return $"Variant::Structure(VariantType(VariantType::Structure, {apiType.FullNameNative}::TypeInitializer.GetType()), {(typeInfo.IsPtr ? "*" + value : value)})"; + } } if (typeInfo.IsPtr && typeInfo.IsConst) @@ -215,6 +217,84 @@ namespace Flax.Build.Bindings return $"Variant({value})"; } + public static string GenerateCppWrapperNativeToVariantType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller) + { + // In-built types + switch (typeInfo.Type) + { + case "void": return "VariantType(VariantType::Void)"; + case "bool": return "VariantType(VariantType::Bool)"; + case "int": + case "int32": return "VariantType(VariantType::Int)"; + case "uint": + case "uint32": return "VariantType(VariantType::Uint)"; + case "int64": return "VariantType(VariantType::Int64)"; + case "uint64": return "VariantType(VariantType::Uint64)"; + case "Real": return "VariantType(VariantType::Real)"; + case "float": return "VariantType(VariantType::Float)"; + case "double": return "VariantType(VariantType::Double)"; + case "StringAnsiView": + case "StringAnsi": + case "StringView": + case "String": return "VariantType(VariantType::String)"; + case "Guid": return "VariantType(VariantType::Guid)"; + case "Asset": return "VariantType(VariantType::Asset)"; + case "Float2": return "VariantType(VariantType::Float2)"; + case "Float3": return "VariantType(VariantType::Float3)"; + case "Float4": return "VariantType(VariantType::Float4)"; + case "Double2": return "VariantType(VariantType::Double2)"; + case "Double3": return "VariantType(VariantType::Double3)"; + case "Double4": return "VariantType(VariantType::Double4)"; + case "Vector2": return "VariantType(VariantType::Vector2)"; + case "Vector3": return "VariantType(VariantType::Vector3)"; + case "Vector4": return "VariantType(VariantType::Vector4)"; + case "Int2": return "VariantType(VariantType::Int2)"; + case "Int3": return "VariantType(VariantType::Int3)"; + case "Int4": return "VariantType(VariantType::Int4)"; + case "Color": return "VariantType(VariantType::Color)"; + case "BoundingBox": return "VariantType(VariantType::BoundingBox)"; + case "BoundingSphere": return "VariantType(VariantType::BoundingSphere)"; + case "Quaternion": return "VariantType(VariantType::Quaternion)"; + case "Transform": return "VariantType(VariantType::Transform)"; + case "Rectangle": return "VariantType(VariantType::Rectangle)"; + case "Ray": return "VariantType(VariantType::Ray)"; + case "Matrix": return "VariantType(VariantType::Matrix)"; + case "Type": return "VariantType(VariantType::Typename)"; + } + + // Array + if (typeInfo.IsArray) + return "VariantType(VariantType::Array)"; + if ((typeInfo.Type == "Array" || typeInfo.Type == "Span") && typeInfo.GenericArgs != null) + { + var elementType = FindApiTypeInfo(buildData, typeInfo.GenericArgs[0], caller); + var elementName = $"{(elementType != null ? elementType.FullNameManaged : typeInfo.GenericArgs[0].Type)}[]"; + return $"VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length}))"; + } + if (typeInfo.Type == "Dictionary" && typeInfo.GenericArgs != null) + return "VariantType(VariantType::Dictionary)"; + + // Scripting type + var apiType = FindApiTypeInfo(buildData, typeInfo, caller); + if (apiType != null) + { + var fullname = apiType.FullNameManaged; + if (apiType.IsEnum) + return $"VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + if (apiType.IsStruct) + { + if (apiType.IsInBuild) + return $"VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + return $"VariantType(VariantType::Structure, {apiType.FullNameNative}::TypeInitializer.GetType())"; + } + if (apiType.IsClass) + return $"VariantType(VariantType::Object, {apiType.FullNameNative}::TypeInitializer.GetType())"; + } + + // Unknown + return "VariantType()"; + } + public static string GenerateCppWrapperVariantToNative(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, string value) { if (typeInfo.Type == "Variant") @@ -393,7 +473,7 @@ namespace Flax.Build.Bindings return "Scripting::FindClass(\"" + managedType + "\")"; } - private static string GenerateCppGetNativeType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, FunctionInfo functionInfo) + private static string GenerateCppGetNativeType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller, FunctionInfo functionInfo = null) { CppIncludeFiles.Add("Engine/Scripting/ManagedCLR/MClass.h"); @@ -922,6 +1002,22 @@ namespace Flax.Build.Bindings if (typeInfo.Type == "BytesContainer" && typeInfo.GenericArgs == null) return $"MUtils::ToArray({value})"; + // Dictionary + if (typeInfo.Type == "Dictionary" && typeInfo.GenericArgs != null) + { + CppIncludeFiles.Add("Engine/Scripting/Internal/ManagedDictionary.h"); + var keyClass = GenerateCppGetNativeType(buildData, typeInfo.GenericArgs[0], caller); + var valueClass = GenerateCppGetNativeType(buildData, typeInfo.GenericArgs[1], caller); + return $"ManagedDictionary::ToManaged({value}, {keyClass}, {valueClass})"; + } + + // HashSet + if (typeInfo.Type == "HashSet" && typeInfo.GenericArgs != null) + { + // TODO: automatic converting managed-native for HashSet + throw new NotImplementedException("TODO: converting managed HashSet to native"); + } + // Construct native typename for MUtils template argument var nativeType = new StringBuilder(64); nativeType.Append(typeInfo.Type); @@ -1751,7 +1847,23 @@ namespace Flax.Build.Bindings { if (!functionInfo.IsVirtual) continue; - contents.AppendLine($" scriptVTable[{scriptVTableIndex++}] = mclass->GetMethod(\"{functionInfo.Name}\", {functionInfo.Parameters.Count});"); + + // Don't use exact signature for parameter-less methods or the ones without duplicates + if (functionInfo.Parameters.Count == 0 || !classInfo.Functions.Any(x => x != functionInfo && x.Parameters.Count == functionInfo.Parameters.Count && x.Name == functionInfo.Name)) + contents.AppendLine($" scriptVTable[{scriptVTableIndex++}] = mclass->GetMethod(\"{functionInfo.Name}\", {functionInfo.Parameters.Count});"); + else + { + contents.AppendLine(" {"); + contents.AppendLine(" ScriptingTypeMethodSignature signature;"); + contents.AppendLine($" signature.Name = StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length});"); + contents.AppendLine($" signature.Params.Resize({functionInfo.Parameters.Count});"); + for (var i = 0; i < functionInfo.Parameters.Count; i++) + contents.AppendLine($" signature.Params[{i}] = {{ {GenerateCppWrapperNativeToVariantType(buildData, functionInfo.Parameters[i].Type, classInfo)}, {(functionInfo.Parameters[i].IsOut ? "true" : "false")} }};"); + contents.AppendLine($" scriptVTable[{scriptVTableIndex++}] = mclass->GetMethod(signature);"); + if (buildData.Configuration != TargetConfiguration.Release) + contents.AppendLine($" ASSERT(scriptVTable[{scriptVTableIndex - 1}]);"); + contents.AppendLine(" }"); + } } contents.AppendLine(" }"); contents.AppendLine(""); @@ -2701,10 +2813,22 @@ namespace Flax.Build.Bindings { contents.AppendLine(" Variant* parameters = nullptr;"); } + if (functionInfo.Parameters.Count != 0) + { + // Build method signature to find method using exact parameter types to match on name collisions + contents.AppendLine(" ScriptingTypeMethodSignature signature;"); + contents.AppendLine($" signature.Name = StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length});"); + contents.AppendLine($" signature.Params.Resize({functionInfo.Parameters.Count});"); + for (var i = 0; i < functionInfo.Parameters.Count; i++) + contents.AppendLine($" signature.Params[{i}] = {{ parameters[{i}].Type, {(functionInfo.Parameters[i].IsOut ? "true" : "false")} }};"); + } contents.AppendLine(" auto typeHandle = Object->GetTypeHandle();"); contents.AppendLine(" while (typeHandle)"); contents.AppendLine(" {"); - contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});"); + if (functionInfo.Parameters.Count == 0) + contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});"); + else + contents.AppendLine(" auto method = typeHandle.Module->FindMethod(typeHandle, signature);"); contents.AppendLine(" if (method)"); contents.AppendLine(" {"); contents.AppendLine(" Variant __result;"); @@ -2977,7 +3101,9 @@ namespace Flax.Build.Bindings header.Append($"{wrapperName}Array(const {valueType}* v, const int32 length)").AppendLine(); header.Append('{').AppendLine(); header.Append(" Variant result;").AppendLine(); - header.Append(" result.SetType(VariantType(VariantType::Array));").AppendLine(); + var apiType = FindApiTypeInfo(buildData, valueType, moduleInfo); + var elementName = $"{(apiType != null ? apiType.FullNameManaged : valueType.Type)}[]"; + header.Append($" result.SetType(VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length})));").AppendLine(); header.Append(" auto* array = reinterpret_cast*>(result.AsData);").AppendLine(); header.Append(" array->Resize(length);").AppendLine(); header.Append(" for (int32 i = 0; i < length; i++)").AppendLine(); @@ -3345,13 +3471,9 @@ namespace Flax.Build.Bindings contents.AppendLine($"extern \"C\" BinaryModule* GetBinaryModule{binaryModuleName}()"); contents.AppendLine("{"); if (useCSharp) - { contents.AppendLine($" static NativeBinaryModule module(\"{binaryModuleName}\");"); - } else - { contents.AppendLine($" static NativeOnlyBinaryModule module(\"{binaryModuleName}\");"); - } contents.AppendLine(" return &module;"); contents.AppendLine("}"); if (project.VersionControlBranch.Length != 0) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 15f42c3ae..ef5f959c8 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; +using System.Threading; using Flax.Build; using Flax.Build.Platforms; @@ -199,9 +200,14 @@ namespace Flax.Deploy var dmgPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.dmg"); Log.Info(string.Empty); Log.Info("Building disk image..."); + Thread.Sleep(100); if (File.Exists(dmgPath)) + { + Log.Verbose("Removing old image"); File.Delete(dmgPath); - Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\" -force", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + Thread.Sleep(100); + } + Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); CodeSign(dmgPath); Log.Info("Output disk image size: " + Utilities.GetFileSize(dmgPath)); diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs index a1018368b..00838a73d 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs @@ -67,7 +67,8 @@ namespace Flax.Build.Platforms if (subDirs.Length != 0) { Utilities.SortVersionDirectories(subDirs); - FindNDK(subDirs.Last()); + for (int i = subDirs.Length - 1; i >= 0 && !IsValid; i--) + FindNDK(subDirs[i]); } if (!IsValid) @@ -109,6 +110,14 @@ namespace Flax.Build.Platforms } if (IsValid) { + var minVersion = new Version(27, 0); // NDK 27 (and newer) contains the libc++_shared.so with 16kb alignment + if (Version < minVersion) + { + IsValid = false; + Log.Verbose(RootPath); + Log.Error(string.Format("Unsupported Android NDK version {0}. Minimum supported is {1}.", Version, minVersion)); + return; + } RootPath = sdkPath; Log.Info(string.Format("Found Android NDK {1} at {0}", RootPath, Version)); } diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs index ab332fdb1..34b77ae5a 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs @@ -104,6 +104,9 @@ namespace Flax.Build.Platforms args.Add("-fno-function-sections"); } + // Support 16kb pages + args.Add("-D__BIONIC_NO_PAGE_SIZE_MACRO"); + switch (Architecture) { case TargetArchitecture.x86: @@ -195,10 +198,10 @@ namespace Flax.Build.Platforms { // https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#libc var ndkPath = AndroidNdk.Instance.RootPath; - var libCppSharedPath = Path.Combine(ndkPath, "sources/cxx-stl/llvm-libc++/libs/", GetAbiName(Architecture), "libc++_shared.so"); // NDK24 (and older) location + var libCppSharedPath = Path.Combine(ndkPath, "toolchains/llvm/prebuilt", AndroidSdk.GetHostName(), "sysroot/usr/lib/", GetToolchainName(TargetPlatform.Android, Architecture), "libc++_shared.so"); // NDK25+ location if (!File.Exists(libCppSharedPath)) { - libCppSharedPath = Path.Combine(ndkPath, "toolchains/llvm/prebuilt", AndroidSdk.GetHostName(), "sysroot/usr/lib/", GetToolchainName(TargetPlatform.Android, Architecture), "libc++_shared.so"); // NDK25+ location + libCppSharedPath = Path.Combine(ndkPath, "sources/cxx-stl/llvm-libc++/libs/", GetAbiName(Architecture), "libc++_shared.so"); // NDK24 (and older) location if (!File.Exists(libCppSharedPath)) throw new Exception($"Missing Android NDK `libc++_shared.so` for architecture {Architecture}."); }