Merge remote-tracking branch 'origin/master' into 1.12

# Conflicts:
#	Content/Editor/MaterialTemplates/Deformable.shader
#	Flax.flaxproj
#	Source/Engine/Content/Content.h
#	Source/Engine/Serialization/JsonTools.cpp
This commit is contained in:
Wojtek Figat
2026-04-01 17:14:21 +02:00
115 changed files with 2933 additions and 1074 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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.",

View File

@@ -19,7 +19,7 @@ namespace FlaxEditor.Content.GUI
/// <summary>
/// Gets the target node.
/// </summary>
public ContentTreeNode TargetNode { get; }
public ContentFolderTreeNode TargetNode { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ContentNavigationButton"/> class.
@@ -28,7 +28,7 @@ namespace FlaxEditor.Content.GUI
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="height">The height.</param>
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;

View File

@@ -59,7 +59,7 @@ namespace FlaxEditor.Content
/// <summary>
/// Gets the content node.
/// </summary>
public ContentTreeNode Node { get; }
public ContentFolderTreeNode Node { get; }
/// <summary>
/// The subitems of this folder.
@@ -72,7 +72,7 @@ namespace FlaxEditor.Content
/// <param name="type">The folder type.</param>
/// <param name="path">The path to the item.</param>
/// <param name="node">The folder parent node.</param>
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;
}
}

View File

@@ -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;
/// <summary>
/// Content folder tree node.
/// </summary>
/// <seealso cref="TreeNode" />
public class ContentFolderTreeNode : TreeNode
{
private DragItems _dragOverItems;
private DragActors _dragActors;
private List<Rectangle> _highlights;
/// <summary>
/// The folder.
/// </summary>
protected ContentFolder _folder;
/// <summary>
/// Whether this node can be deleted.
/// </summary>
public virtual bool CanDelete => true;
/// <summary>
/// Whether this node can be duplicated.
/// </summary>
public virtual bool CanDuplicate => true;
/// <summary>
/// Gets the content folder item.
/// </summary>
public ContentFolder Folder => _folder;
/// <summary>
/// Gets the type of the folder.
/// </summary>
public ContentFolderType FolderType => _folder.FolderType;
/// <summary>
/// Returns true if that folder can import/manage scripts.
/// </summary>
public bool CanHaveScripts => _folder.CanHaveScripts;
/// <summary>
/// Returns true if that folder can import/manage assets.
/// </summary>
/// <returns>True if can contain assets for project, otherwise false</returns>
public bool CanHaveAssets => _folder.CanHaveAssets;
/// <summary>
/// Gets the parent node.
/// </summary>
public ContentFolderTreeNode ParentNode => Parent as ContentFolderTreeNode;
/// <summary>
/// Gets the folder path.
/// </summary>
public string Path => _folder.Path;
/// <summary>
/// Gets the navigation button label.
/// </summary>
public virtual string NavButtonLabel => _folder.ShortName;
/// <summary>
/// Initializes a new instance of the <see cref="ContentFolderTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="path">The folder path.</param>
public ContentFolderTreeNode(ContentFolderTreeNode parent, string path)
: this(parent, parent?.FolderType ?? ContentFolderType.Other, path)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentFolderTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="type">The folder type.</param>
/// <param name="path">The folder path.</param>
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);
}
/// <summary>
/// Updates the custom arrow rectangle so it stays aligned with the current layout.
/// </summary>
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);
}
/// <inheritdoc />
public override void PerformLayout(bool force = false)
{
base.PerformLayout(force);
UpdateCustomArrowRect();
}
/// <summary>
/// Shows the rename popup for the item.
/// </summary>
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); };
}
/// <summary>
/// Updates the query search filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
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<Rectangle>(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;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override void OnDestroy()
{
// Delete folder item
_folder.Dispose();
base.OnDestroy();
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void OnDragLeaveHeader()
{
_dragOverItems?.OnDragLeave();
_dragActors?.OnDragLeave();
base.OnDragLeaveHeader();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
protected override void DoDragDrop()
{
DoDragDrop(DragItems.GetDragData(_folder));
}
/// <inheritdoc />
protected override void OnLongPress()
{
Select();
StartRenaming();
}
/// <inheritdoc />
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);
}
}

View File

@@ -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;
/// <summary>
/// Tree node for non-folder content items.
/// </summary>
public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner
{
private List<Rectangle> _highlights;
/// <summary>
/// The content item.
/// </summary>
public ContentItem Item { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ContentItemTreeNode"/> class.
/// </summary>
/// <param name="item">The content item.</param>
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;
}
/// <summary>
/// Updates the query search filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
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<Rectangle>(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;
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void DoDragDrop()
{
DoDragDrop(DragItems.GetDragData(Item));
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
void IContentItemOwner.OnItemDeleted(ContentItem item)
{
}
/// <inheritdoc />
void IContentItemOwner.OnItemRenamed(ContentItem item)
{
UpdateDisplayedName();
}
/// <inheritdoc />
void IContentItemOwner.OnItemReimported(ContentItem item)
{
}
/// <inheritdoc />
void IContentItemOwner.OnItemDispose(ContentItem item)
{
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override void OnDestroy()
{
Item.RemoveReference(this);
base.OnDestroy();
}
/// <summary>
/// Updates the text of the node.
/// </summary>
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;
}
}

View File

@@ -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
{
/// <summary>
/// Content folder tree node.
/// </summary>
/// <seealso cref="TreeNode" />
public class ContentTreeNode : TreeNode
{
private DragItems _dragOverItems;
private List<Rectangle> _highlights;
/// <summary>
/// The folder.
/// </summary>
protected ContentFolder _folder;
/// <summary>
/// Whether this node can be deleted.
/// </summary>
public virtual bool CanDelete => true;
/// <summary>
/// Whether this node can be duplicated.
/// </summary>
public virtual bool CanDuplicate => true;
/// <summary>
/// Gets the content folder item.
/// </summary>
public ContentFolder Folder => _folder;
/// <summary>
/// Gets the type of the folder.
/// </summary>
public ContentFolderType FolderType => _folder.FolderType;
/// <summary>
/// Returns true if that folder can import/manage scripts.
/// </summary>
public bool CanHaveScripts => _folder.CanHaveScripts;
/// <summary>
/// Returns true if that folder can import/manage assets.
/// </summary>
/// <returns>True if can contain assets for project, otherwise false</returns>
public bool CanHaveAssets => _folder.CanHaveAssets;
/// <summary>
/// Gets the parent node.
/// </summary>
public ContentTreeNode ParentNode => Parent as ContentTreeNode;
/// <summary>
/// Gets the folder path.
/// </summary>
public string Path => _folder.Path;
/// <summary>
/// Gets the navigation button label.
/// </summary>
public virtual string NavButtonLabel => _folder.ShortName;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="path">The folder path.</param>
public ContentTreeNode(ContentTreeNode parent, string path)
: this(parent, parent?.FolderType ?? ContentFolderType.Other, path)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="type">The folder type.</param>
/// <param name="path">The folder path.</param>
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;
}
/// <summary>
/// Shows the rename popup for the item.
/// </summary>
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); };
}
/// <summary>
/// Updates the query search filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
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<Rectangle>(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;
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override DragDropEffect OnDragEnterHeader(DragData data)
{
if (_dragOverItems == null)
_dragOverItems = new DragItems(ValidateDragItem);
_dragOverItems.OnDragEnter(data);
return GetDragEffect(data);
}
/// <inheritdoc />
protected override DragDropEffect OnDragMoveHeader(DragData data)
{
return GetDragEffect(data);
}
/// <inheritdoc />
protected override void OnDragLeaveHeader()
{
_dragOverItems.OnDragLeave();
base.OnDragLeaveHeader();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
protected override void DoDragDrop()
{
DoDragDrop(DragItems.GetDragData(_folder));
}
/// <inheritdoc />
protected override void OnLongPress()
{
Select();
StartRenaming();
}
/// <inheritdoc />
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);
}
}
}

View File

@@ -7,8 +7,8 @@ namespace FlaxEditor.Content
/// <summary>
/// Content tree node used for main directories.
/// </summary>
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
public class MainContentTreeNode : ContentTreeNode
/// <seealso cref="ContentFolderTreeNode" />
public class MainContentFolderTreeNode : ContentFolderTreeNode
{
private FileSystemWatcher _watcher;
@@ -19,12 +19,12 @@ namespace FlaxEditor.Content
public override bool CanDuplicate => false;
/// <summary>
/// Initializes a new instance of the <see cref="MainContentTreeNode"/> class.
/// Initializes a new instance of the <see cref="MainContentFolderTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent project.</param>
/// <param name="type">The folder type.</param>
/// <param name="path">The folder path.</param>
public MainContentTreeNode(ProjectTreeNode parent, ContentFolderType type, string path)
public MainContentFolderTreeNode(ProjectFolderTreeNode parent, ContentFolderType type, string path)
: base(parent, type, path)
{
_watcher = new FileSystemWatcher(path)

View File

@@ -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
/// <summary>
/// Root tree node for the project workspace.
/// </summary>
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
public sealed class ProjectTreeNode : ContentTreeNode
/// <seealso cref="ContentFolderTreeNode" />
public sealed class ProjectFolderTreeNode : ContentFolderTreeNode
{
/// <summary>
/// The project/
@@ -18,18 +19,18 @@ namespace FlaxEditor.Content
/// <summary>
/// The project content directory.
/// </summary>
public MainContentTreeNode Content;
public MainContentFolderTreeNode Content;
/// <summary>
/// The project source code directory.
/// </summary>
public MainContentTreeNode Source;
public MainContentFolderTreeNode Source;
/// <summary>
/// Initializes a new instance of the <see cref="ProjectTreeNode"/> class.
/// Initializes a new instance of the <see cref="ProjectFolderTreeNode"/> class.
/// </summary>
/// <param name="project">The project.</param>
public ProjectTreeNode(ProjectInfo project)
public ProjectFolderTreeNode(ProjectInfo project)
: base(null, project.ProjectFolderPath)
{
Project = project;
@@ -48,9 +49,29 @@ namespace FlaxEditor.Content
/// <inheritdoc />
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);
}
}

View File

@@ -5,13 +5,13 @@ namespace FlaxEditor.Content
/// <summary>
/// Root tree node for the content workspace.
/// </summary>
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
public sealed class RootContentTreeNode : ContentTreeNode
/// <seealso cref="ContentFolderTreeNode" />
public sealed class RootContentFolderTreeNode : ContentFolderTreeNode
{
/// <summary>
/// Initializes a new instance of the <see cref="RootContentTreeNode"/> class.
/// Initializes a new instance of the <see cref="RootContentFolderTreeNode"/> class.
/// </summary>
public RootContentTreeNode()
public RootContentFolderTreeNode()
: base(null, string.Empty)
{
}

View File

@@ -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<Prefab>(actor.PrefabID);
// TODO: don't stall here?
if (prefab && !prefab.WaitForLoaded())
var prefab = FlaxEngine.Content.Load<Prefab>(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;

View File

@@ -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);
}
/// <inheritdoc />

View File

@@ -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
}
/// <inheritdoc />
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);
}
}

View File

@@ -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)
{

View File

@@ -24,22 +24,22 @@ namespace FlaxEditor.Modules
private bool _rebuildInitFlag;
private int _itemsCreated;
private int _itemsDeleted;
private readonly HashSet<MainContentTreeNode> _dirtyNodes = new HashSet<MainContentTreeNode>();
private readonly HashSet<MainContentFolderTreeNode> _dirtyNodes = new HashSet<MainContentFolderTreeNode>();
/// <summary>
/// The project directory.
/// </summary>
public ProjectTreeNode Game { get; private set; }
public ProjectFolderTreeNode Game { get; private set; }
/// <summary>
/// The engine directory.
/// </summary>
public ProjectTreeNode Engine { get; private set; }
public ProjectFolderTreeNode Engine { get; private set; }
/// <summary>
/// The list of all projects workspace directories (including game, engine and plugins projects).
/// </summary>
public readonly List<ProjectTreeNode> Projects = new List<ProjectTreeNode>();
public readonly List<ProjectFolderTreeNode> Projects = new List<ProjectFolderTreeNode>();
/// <summary>
/// The list with all content items proxy objects. Use <see cref="AddProxy"/> and <see cref="RemoveProxy"/> to modify this or <see cref="Rebuild"/> to refresh database when adding new item proxy types.
@@ -116,7 +116,7 @@ namespace FlaxEditor.Modules
/// </summary>
/// <param name="project">The project.</param>
/// <returns>The project workspace or null if not loaded into database.</returns>
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)

View File

@@ -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;
/// <summary>
/// Gets or sets a value that indicates whether the main viewports <see cref="Gizmo.DirectionGizmo"/> is visible.
/// </summary>
[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;
/// <summary>
/// Gets or sets a value by which the main viewports <see cref="Gizmo.DirectionGizmo"/> size is multiplied with.
/// </summary>
[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;
/// <summary>
/// Gets or sets a value for the opacity of the main viewports <see cref="Gizmo.DirectionGizmo"/> background.
/// </summary>
[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;
/// <summary>
/// Gets or sets a value for the opacity of the main viewports <see cref="Gizmo.DirectionGizmo"/>.
/// </summary>
[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;
/// <summary>
/// Gets or sets a value for the opacity of the main viewports <see cref="Gizmo.DirectionGizmo"/>.
/// </summary>
[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;
}
}

View File

@@ -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;
}
/// <inheritdoc />
@@ -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);

View File

@@ -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)
{

View File

@@ -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;
}
/// <inheritdoc />
@@ -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[]
{

View File

@@ -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();
}
}
/// <inheritdoc />

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}
/// <inheritdoc />
protected override Color FooterColor => new Color(200, 11, 112);
protected override Color ArchetypeColor => new Color(200, 11, 112);
/// <inheritdoc />
public override void OnLoaded(SurfaceNodeActions action)

View File

@@ -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),

View File

@@ -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[]
{

View File

@@ -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);
}
}
/// <summary>

View File

@@ -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
}
}
/// <inheritdoc />
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
/// <summary>
/// The particle module node elements offset applied to controls to reduce default surface node header thickness.
/// </summary>
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;

View File

@@ -74,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes
/// <summary>
/// The header height.
/// </summary>
public const float HeaderHeight = FlaxEditor.Surface.Constants.NodeHeaderSize;
public const float HeaderHeight = FlaxEditor.Surface.Constants.NodeHeaderHeight;
/// <summary>
/// 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[]
{

View File

@@ -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),

View File

@@ -467,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes
Create = (id, context, arch, groupArch) => new CurveNode<T>(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<T>
{
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;
}
/// <inheritdoc />
public override void Resize(float width, float height)
{
// Do nothing so the input and output boxes do not change position
}
/// <inheritdoc />
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 },

View File

@@ -13,46 +13,61 @@ namespace FlaxEditor.Surface
/// <summary>
/// The node close button size.
/// </summary>
public const float NodeCloseButtonSize = 12.0f;
public const float NodeCloseButtonSize = 10.0f;
/// <summary>
/// The node close button margin from the edges.
/// </summary>
public const float NodeCloseButtonMargin = 2.0f;
public const float NodeCloseButtonMargin = 5.0f;
/// <summary>
/// The node header height.
/// </summary>
public const float NodeHeaderSize = 28.0f;
public const float NodeHeaderHeight = 25.0f;
/// <summary>
/// The scale of the header text.
/// </summary>
public const float NodeHeaderTextScale = 0.8f;
/// <summary>
/// The node footer height.
/// </summary>
public const float NodeFooterSize = 4.0f;
public const float NodeFooterSize = 2.0f;
/// <summary>
/// The node left margin.
/// The horizontal node margin.
/// </summary>
public const float NodeMarginX = 5.0f;
public const float NodeMarginX = 6.0f;
/// <summary>
/// The node right margin.
/// The vertical node right margin.
/// </summary>
public const float NodeMarginY = 5.0f;
public const float NodeMarginY = 8.0f;
/// <summary>
/// The box position offset on the x axis.
/// The size of the row that is started by a box.
/// </summary>
public const float BoxOffsetX = 2.0f;
public const float BoxRowHeight = 19.0f;
/// <summary>
/// The box size (with and height).
/// </summary>
public const float BoxSize = 20.0f;
public const float BoxSize = 15.0f;
/// <summary>
/// The node layout offset on the y axis (height of the boxes rows, etc.). It's used to make the design more consistent.
/// </summary>
public const float LayoutOffsetY = 20.0f;
public const float LayoutOffsetY = 24.0f;
/// <summary>
/// The offset between the box text and the box
/// </summary>
public const float BoxTextOffset = 1.65f;
/// <summary>
/// The width of the rectangle used to draw the box text.
/// </summary>
public const float BoxTextRectWidth = 500.0f;
}
}

View File

@@ -33,7 +33,7 @@ namespace FlaxEditor.Surface.Elements
/// <inheritdoc />
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)
{
}

View File

@@ -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;
}
/// <summary>
/// Gets if the box is disabled (user can still connect, but connections will be ignored).
/// </summary>
public bool IsDisabled => !(Enabled && IsActive);
/// <summary>
/// Gets a value indicating whether all connections are disabled.
/// </summary>
public bool AllConnectionsDisabled => Connections.All(c => c.IsDisabled);
/// <inheritdoc />
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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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]);

View File

@@ -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;

View File

@@ -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);
}
/// <inheritdoc />

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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
/// <summary>
/// Gets the actual element position on the y axis.
/// </summary>
public float ActualPositionY => Position.Y + Constants.NodeMarginY + Constants.NodeHeaderSize;
public float ActualPositionY => Position.Y + Constants.NodeMarginY + Constants.NodeHeaderHeight;
/// <summary>
/// Gets the actual element position.
/// </summary>
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);
/// <summary>
/// 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
/// <param name="height">The control height.</param>
/// <param name="tooltip">The control tooltip text.</param>
/// <returns>The archetype.</returns>
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,

View File

@@ -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

View File

@@ -78,6 +78,11 @@ namespace FlaxEditor.Surface
/// </summary>
VariableValuesSize = 2048,
/// <summary>
/// Node has fixed size defined and should not use automatic layout.
/// </summary>
FixedSize = 4096,
/// <summary>
/// Node can be used in the all visual graphs.
/// </summary>

View File

@@ -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)

View File

@@ -74,6 +74,12 @@ namespace FlaxEditor.Surface
Resize(size.X, size.Y);
}
/// <inheritdoc />
public override void ResizeAuto()
{
// Do nothing, we want to put full control of node size into the users hands
}
/// <inheritdoc />
public override void Draw()
{

View File

@@ -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);

View File

@@ -40,6 +40,13 @@ namespace FlaxEditor.Surface
[HideInEditor]
public class SurfaceNode : SurfaceControl
{
internal const float ShadowOffset = 2.25f;
/// <summary>
/// If true, draws a basic rectangle shadow behind the node. Disable to hide shadow or if the node is drawing a custom shadow.
/// </summary>
internal virtual bool DrawBasicShadow => true;
/// <summary>
/// The box to draw a highlight around. Drawing will be skipped if null.
/// </summary>
@@ -55,6 +62,11 @@ namespace FlaxEditor.Surface
/// </summary>
protected Rectangle _headerRect;
/// <summary>
/// The header text rectangle (local space).
/// </summary>
protected Rectangle _headerTextRect;
/// <summary>
/// The close button rectangle (local space).
/// </summary>
@@ -123,7 +135,7 @@ namespace FlaxEditor.Surface
/// <param name="nodeArch">The node archetype.</param>
/// <param name="groupArch">The group archetype.</param>
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;
/// <summary>
/// Gets the color of the footer of the node.
/// Gets the color of the header of the node.
/// </summary>
protected virtual Color FooterColor => GroupArchetype.Color;
protected virtual Color ArchetypeColor => GroupArchetype.Color;
private Float2 mouseDownMousePosition;
@@ -161,7 +173,7 @@ namespace FlaxEditor.Surface
/// <returns>The node control total size.</returns>
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);
}
/// <summary>
@@ -169,7 +181,7 @@ namespace FlaxEditor.Surface
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
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();
}
/// <summary>
@@ -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<SurfaceNode> UpdateStack = new List<SurfaceNode>();
/// <summary>
/// Updates dependant/independent boxes types.
/// Updates dependent/independent boxes types.
/// </summary>
public void UpdateBoxesTypes()
{
@@ -776,6 +802,24 @@ namespace FlaxEditor.Surface
return output;
}
/// <summary>
/// Draws the close button inside of the <paramref name="rect"/>.
/// </summary>
/// <param name="rect">The rectangle to draw the close button in.</param>
/// <param name="color">The color of the close button.</param>
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;
}
/// <summary>
/// Draws all the connections between surface objects related to this node.
/// </summary>
@@ -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();
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
@@ -956,6 +1010,8 @@ namespace FlaxEditor.Surface
Surface.AddBatchedUndoAction(new EditNodeValuesAction(this, before, graphEdited));
_isDuringValuesEditing = false;
UpdateSize();
}
/// <summary>
@@ -990,6 +1046,8 @@ namespace FlaxEditor.Surface
}
_isDuringValuesEditing = false;
UpdateSize();
}
internal void SetIsDuringValuesEditing(bool value)
@@ -998,7 +1056,7 @@ namespace FlaxEditor.Surface
}
/// <summary>
/// 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.
/// </summary>
/// <param name="values">The input values array.</param>
public virtual void SetValuesPaste(object[] values)
@@ -1022,16 +1080,18 @@ namespace FlaxEditor.Surface
public virtual void ConnectionTick(Box box)
{
UpdateBoxesTypes();
UpdateSize();
}
/// <inheritdoc />
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

View File

@@ -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
/// </summary>
public Texture Background;
/// <summary>
/// The color used as a surface background.
/// </summary>
public Color BackgroundColor;
/// <summary>
/// Boxes drawing callback.
/// </summary>
@@ -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;
}
/// <summary>
/// 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.
/// </summary>
public static Func<Editor, SurfaceStyle> 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);

View File

@@ -65,31 +65,68 @@ namespace FlaxEditor.Surface
/// </summary>
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);
}
}
}

View File

@@ -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

View File

@@ -41,7 +41,7 @@ namespace FlaxEditor.Actions
ActionString = name;
_pasteParent = pasteParent;
_idsMapping = new Dictionary<Guid, Guid>(objectIds.Length * 4);
_idsMapping = new Dictionary<Guid, Guid>(objectIds.Length);
for (int i = 0; i < objectIds.Length; i++)
{
_idsMapping[objectIds[i]] = Guid.NewGuid();
@@ -72,13 +72,24 @@ namespace FlaxEditor.Actions
/// <summary>
/// Links the broken parent reference (missing parent). By default links the actor to the first scene.
/// </summary>
/// <param name="actor">The actor.</param>
protected virtual void LinkBrokenParentReference(Actor actor)
/// <param name="actorNode">The actor node.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="actorNode">The actor node.</param>
protected virtual void CheckBrokenParentReference(ActorNode actorNode)
{
// Ensure pasted object ends up on a scene
if (actorNode.Actor.Scene == null)
LinkBrokenParentReference(actorNode);
}
/// <inheritdoc />
@@ -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<string, bool> foundNamesResults = new();

View File

@@ -166,7 +166,6 @@ namespace FlaxEditor.Viewport
#endif
// Input
internal bool _disableInputUpdate;
private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown;
private Float2 _startPos;

View File

@@ -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;
/// <summary>
@@ -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;
}
/// <inheritdoc />

View File

@@ -264,10 +264,17 @@ namespace FlaxEditor.Windows.Assets
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void CheckBrokenParentReference(ActorNode actorNode)
{
if (actorNode.Actor.Scene != null || actorNode.Root != _window.Graph.Root)
LinkBrokenParentReference(actorNode);
}
/// <inheritdoc />

View File

@@ -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;
}

View File

@@ -10,15 +10,47 @@ namespace FlaxEditor.Windows
{
public partial class ContentWindow
{
private static readonly List<ContentTreeNode> NavUpdateCache = new List<ContentTreeNode>(8);
private static readonly List<ContentFolderTreeNode> NavUpdateCache = new List<ContentFolderTreeNode>(8);
private void OnTreeSelectionChanged(List<TreeNode> from, List<TreeNode> 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.
/// </summary>
/// <param name="target">The target.</param>
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
/// </summary>
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
/// <summary>
/// Gets the selected tree node.
/// </summary>
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;
}
}
/// <summary>
/// Gets the current view folder.
/// </summary>
public ContentFolder CurrentViewFolder => SelectedNode?.Folder;
private TreeNode GetActiveTreeSelection(List<TreeNode> selection)
{
if (selection == null || selection.Count == 0)
return null;
return _showAllContentInTree && selection.Count > 1
? selection[^1]
: selection[0];
}
/// <summary>
/// Shows the root folder.
/// </summary>
@@ -236,5 +294,10 @@ namespace FlaxEditor.Windows
{
_tree.Select(_root);
}
private void SaveLastViewedFolder(ContentFolderTreeNode node)
{
Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, node?.Path ?? string.Empty);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<string> _expandedFolderPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private bool _renameInTree;
private RootContentTreeNode _root;
private RootContentFolderTreeNode _root;
private bool _navigationUnlocked;
private readonly Stack<ContentTreeNode> _navigationUndo = new Stack<ContentTreeNode>(32);
private readonly Stack<ContentTreeNode> _navigationRedo = new Stack<ContentTreeNode>(32);
private readonly Stack<ContentFolderTreeNode> _navigationUndo = new Stack<ContentFolderTreeNode>(32);
private readonly Stack<ContentFolderTreeNode> _navigationRedo = new Stack<ContentFolderTreeNode>(32);
private NewItem _newElement;
@@ -134,6 +144,9 @@ namespace FlaxEditor.Windows
}
}
internal bool IsTreeOnlyMode => _showAllContentInTree;
internal SortType CurrentSortType => _sortType;
/// <summary>
/// Initializes a new instance of the <see cref="ContentWindow"/> class.
/// </summary>
@@ -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;
}
}
/// <summary>
/// Opens the specified content item.
/// </summary>
@@ -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;
}
/// <summary>
/// Refreshes the current view items collection.
/// </summary>
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.
/// </summary>
/// <param name="target">The target location.</param>
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<ContentItem>(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);
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
@@ -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
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
UpdateNavigationBarBounds();
base.PerformLayoutBeforeChildren();
}
/// <inheritdoc />
protected override void PerformLayoutAfterChildren()
{
base.PerformLayoutAfterChildren();
UpdateNavigationBarBounds();
}
/// <inheritdoc />
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());
}
/// <inheritdoc />
@@ -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();
}
/// <inheritdoc />
@@ -1304,6 +1834,7 @@ namespace FlaxEditor.Windows
{
_split.SplitterValue = 0.2f;
_view.ViewScale = 1.0f;
_showAllContentInTree = false;
}
/// <inheritdoc />
@@ -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();
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -98,6 +98,9 @@ namespace
DateTime LastWorkspaceDiscovery;
CriticalSection WorkspaceDiscoveryLocker;
#endif
#if USE_EDITOR
Dictionary<Guid, HashSet<BinaryAsset*>> 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<BinaryAsset>(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

View File

@@ -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

View File

@@ -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());

View File

@@ -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();
}
}
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<FlaxChunk>();
@@ -1135,7 +1135,7 @@ bool FlaxStorage::Create(WriteStream* stream, Span<AssetInitData> 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

View File

@@ -276,9 +276,9 @@ public:
virtual bool IsPackage() const = 0;
/// <summary>
/// Checks whenever storage container allows the data modifications.
/// Checks whenever storage container doesn't allow modifications.
/// </summary>
virtual bool AllowDataModifications() const = 0;
virtual bool IsReadOnly() const = 0;
/// <summary>
/// Determines whether the specified asset exists in this container.

View File

@@ -253,6 +253,16 @@ namespace FlaxEngine
return new Rectangle(Location + new Float2(x, y), Size);
}
/// <summary>
/// Make offseted rectangle
/// </summary>
/// <param name="offset">Offset (will be applied to X- and Y- axis).</param>
/// <returns>Offseted rectangle.</returns>
public Rectangle MakeOffsetted(float offset)
{
return new Rectangle(Location + offset, Size);
}
/// <summary>
/// Make offseted rectangle
/// </summary>

View File

@@ -253,7 +253,7 @@ public:
/// </summary>
FORCE_INLINE float GetTotalSeconds() const
{
return static_cast<float>(Ticks) / TicksPerSecond;
return (float)((double)Ticks / TicksPerSecond);
}
public:

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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<MethodHolder>(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]

View File

@@ -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;

View File

@@ -428,6 +428,20 @@ namespace FlaxEngine
partial struct VertexElement : IEquatable<VertexElement>
{
/// <summary>
/// Creates the vertex element description.
/// </summary>
/// <param name="type">Element type.</param>
/// <param name="format">Data format.</param>
public VertexElement(Types type, PixelFormat format)
{
Type = type;
Slot = 0;
Offset = 0;
PerInstance = 0;
Format = format;
}
/// <summary>
/// Creates the vertex element description.
/// </summary>

View File

@@ -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;

View File

@@ -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<byte> _data;
private PixelFormat _format;
private int _stride;
private int _stride, _count;
private readonly PixelFormatSampler _sampler;
internal Stream(Span<byte> data, PixelFormat format, int stride)
internal Stream(Span<byte> 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
/// <summary>
/// Gets the count of the items in the stride.
/// </summary>
public int Count => _data.Length / _stride;
public int Count => _count;
/// <summary>
/// Returns true if stream is valid.
@@ -76,6 +78,10 @@ namespace FlaxEngine
/// <returns>Loaded value.</returns>
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
/// <returns>Loaded value.</returns>
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
/// <returns>Loaded value.</returns>
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
/// <returns>Loaded value.</returns>
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
/// <returns>Loaded value.</returns>
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
/// <param name="value">Value to assign.</param>
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);
}
/// <summary>
@@ -143,9 +169,13 @@ namespace FlaxEngine
/// <param name="value">Value to assign.</param>
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);
}
/// <summary>
@@ -155,9 +185,13 @@ namespace FlaxEngine
/// <param name="value">Value to assign.</param>
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);
}
/// <summary>
@@ -167,9 +201,13 @@ namespace FlaxEngine
/// <param name="value">Value to assign.</param>
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);
}
/// <summary>
@@ -179,8 +217,12 @@ namespace FlaxEngine
/// <param name="value">Value to assign.</param>
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);
}
/// <summary>
@@ -190,6 +232,10 @@ namespace FlaxEngine
/// <param name="data">Pointer to the source data.</param>
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<byte>(data.ToPointer(), _data.Length).CopyTo(_data);
}
@@ -199,6 +245,10 @@ namespace FlaxEngine
/// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
public void Set(Span<Float2> 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<byte, Float2>(_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
/// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
public void Set(Span<Float3> 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<byte, Float3>(_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
/// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
public void Set(Span<Color> 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<byte, Color>(_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
/// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
public void Set(Span<uint> 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<byte, uint>(_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
/// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
public void CopyTo(Span<Float2> 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<Float2, byte>(dst));
@@ -325,6 +391,10 @@ namespace FlaxEngine
/// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
public void CopyTo(Span<Float3> 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<Float3, byte>(dst));
@@ -346,6 +416,10 @@ namespace FlaxEngine
/// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
public void CopyTo(Span<Color> 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<Color, byte>(dst));
@@ -367,6 +441,10 @@ namespace FlaxEngine
/// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
public void CopyTo(Span<uint> 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<uint, byte>(dst));
@@ -390,6 +468,17 @@ namespace FlaxEngine
}
}
}
/// <summary>
/// Checks if stream is valid.
/// </summary>
/// <param name="stream">The stream to check.</param>
/// <returns>True if stream is valid, otherwise false.</returns>
[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<byte> data = new Span<byte>();
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);
}
/// <summary>
@@ -596,7 +686,7 @@ namespace FlaxEngine
{
Span<byte> data = new Span<byte>();
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<byte>(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);
}
/// <summary>
@@ -722,6 +813,16 @@ namespace FlaxEngine
set => SetStreamFloat3(VertexElement.Types.Normal, value, PackNormal);
}
/// <summary>
/// Gets or sets the vertex tangent vectors (unpacked, normalized). Null if <see cref="VertexElement.Types.Tangent"/> does not exist in vertex buffers of the mesh.
/// </summary>
/// <remarks>Uses <see cref="Tangent"/> stream to read or write data to the vertex buffer.</remarks>
public Float3[] Tangents
{
get => GetStreamFloat3(VertexElement.Types.Tangent, UnpackNormal);
set => SetStreamFloat3(VertexElement.Types.Tangent, value, PackNormal);
}
/// <summary>
/// Gets or sets the vertex UVs (texcoord channel 0). Null if <see cref="VertexElement.Types.TexCoord"/> does not exist in vertex buffers of the mesh.
/// </summary>
@@ -732,6 +833,70 @@ namespace FlaxEngine
set => SetStreamFloat2(VertexElement.Types.TexCoord, value);
}
/// <summary>
/// Recalculates normal vectors for all vertices.
/// </summary>
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;
}
/// <summary>
/// Recalculates tangent vectors for all vertices based on normals.
/// </summary>
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;

View File

@@ -25,10 +25,10 @@ public:
private:
Span<byte> _data;
PixelFormat _format;
int32 _stride;
int32 _stride, _count;
PixelFormatSampler _sampler;
Stream(Span<byte> data, PixelFormat format, int32 stride);
Stream(Span<byte> data, PixelFormat format, int32 stride, int32 count);
public:
Span<byte> 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());

View File

@@ -38,10 +38,11 @@ namespace
#endif
}
MeshAccessor::Stream::Stream(Span<byte> data, PixelFormat format, int32 stride)
MeshAccessor::Stream::Stream(Span<byte> 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<byte> 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<byte> 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()

View File

@@ -22,7 +22,7 @@ namespace FlaxEngine
/// <summary>
/// Write data function.
/// </summary>
public delegate* unmanaged<void*, ref Float4, void> Write;
public delegate* unmanaged<void*, Float4*, void> Write;
/// <summary>
/// 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<void*, Float4>)read.ToPointer(),
Write = (delegate* unmanaged<void*, ref Float4, void>)write.ToPointer(),
Write = (delegate* unmanaged<void*, Float4*, void>)write.ToPointer(),
};
return pixelSize != 0;
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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<Prefab>(currentPrefabId);
Guid nestedPrefabId, nestedObjectId;
if (prefab && prefab->GetNestedObject(currentObjectId, nestedPrefabId, nestedObjectId))
{
auto nestedPrefab = Content::Load<Prefab>(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<Actor*>& 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<Guid> 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<Actor*>& 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<Actor*>& 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<byte> Actor::ToBytes(const Array<Actor*>& actors)
bool Actor::FromBytes(const Span<byte>& data, Array<Actor*>& 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<byte>& data, Array<Actor*>& output, ISerializeM
}
// Serialized objects ids (for references mapping)
#if 0
Array<Guid> ids;
stream.Read(ids);
int32 objectsCount = ids.Count();
#else
int32 objectsCount;
stream.ReadInt32(&objectsCount);
stream.Move<Guid>(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<int32> order;
order.Resize(objectsCount);
modifier->EngineBuild = engineBuild;
CollectionPoolCache<ActorsCache::SceneObjectsListType>::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<byte>& data, Array<Actor*>& output, ISerializeM
continue;
}
obj->RegisterObject();
// Add to results
Actor* actor = dynamic_cast<Actor*>(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<ActorsCache::ActorsListType>::ScopeCache parents = ActorsCache::ActorsListCache.Get();
parents->EnsureCapacity(output.Count());
@@ -1940,6 +1969,32 @@ bool Actor::FromBytes(const Span<byte>& data, Array<Actor*>& 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<Prefab>(currentPrefabId);
Guid nestedPrefabId, nestedObjectId;
if (prefab && prefab->GetNestedObject(currentObjectId, nestedPrefabId, nestedObjectId))
{
auto nestedPrefab = Content::Load<Prefab>(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<byte>& data, Array<Actor*>& 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

View File

@@ -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

View File

@@ -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());

View File

@@ -60,12 +60,14 @@ public:
/// <summary>
/// Requests the default prefab object instance. Deserializes the prefab objects from the asset. Skips if already done.
/// </summary>
/// <remarks>Default instances of the prefab are read-only and are used internally for objects serialization (prefab diff).</remarks>
/// <returns>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.</returns>
API_FUNCTION() Actor* GetDefaultInstance();
/// <summary>
/// Requests the default prefab object instance. Deserializes the prefab objects from the asset. Skips if already done.
/// </summary>
/// <remarks>Default instances of the prefab are read-only and are used internally for objects serialization (prefab diff).</remarks>
/// <param name="objectId">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.</param>
/// <returns>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.</returns>
API_FUNCTION() SceneObject* GetDefaultInstance(API_PARAM(Ref) const Guid& objectId);

View File

@@ -61,7 +61,7 @@ void SceneObject::BreakPrefabLink()
String SceneObject::GetNamePath(Char separatorChar) const
{
Array<StringView> names;
Array<StringView, InlinedAllocation<8>> names;
const Actor* a = dynamic_cast<const Actor*>(this);
if (!a)
a = GetParent();

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -17,7 +17,6 @@
#include "FlaxEngine.Gen.h"
#include "Scripting.h"
#include "Events.h"
#include "Internal/StdTypesContainer.h"
Dictionary<Pair<ScriptingTypeHandle, StringView>, void(*)(ScriptingObject*, void*, bool)> ScriptingEvents::EventsTable;
Delegate<ScriptingObject*, Span<Variant>, 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

View File

@@ -23,7 +23,7 @@ struct ScriptingTypeMethodSignature
StringAnsiView Name;
VariantType ReturnType;
bool IsStatic;
bool IsStatic = false;
Array<Param, InlinedAllocation<16>> 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);

View File

@@ -188,11 +188,11 @@ public:
MClass* GetBaseClass() const;
/// <summary>
/// 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).
/// </summary>
/// <param name="klass">The class.</param>
/// <param name="checkInterfaces">True if check interfaces, otherwise just base class.</param>
/// <returns>True if this class is a sub class of the specified class.</returns>
/// <returns>True if this class is a subclass of the specified class.</returns>
bool IsSubClassOf(const MClass* klass, bool checkInterfaces = false) const;
/// <summary>
@@ -206,7 +206,7 @@ public:
/// Checks is the provided object instance of this class' type.
/// </summary>
/// <param name="object">The object to check.</param>
/// <returns>True if object is an instance the this class.</returns>
/// <returns>True if object is an instance this class.</returns>
bool IsInstanceOfType(MObject* object) const;
/// <summary>
@@ -227,17 +227,7 @@ public:
/// <param name="numParams">The method parameters count.</param>
/// <param name="checkBaseClasses">True if check base classes when searching for the given method.</param>
/// <returns>The method or null if failed to find it.</returns>
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;
/// <summary>
/// Returns an object referencing a method with the specified name and number of parameters.
@@ -248,6 +238,13 @@ public:
/// <returns>The method or null if failed to get it.</returns>
MMethod* GetMethod(const char* name, int32 numParams = 0) const;
/// <summary>
/// Returns an object referencing a method with the specified signature.
/// </summary>
/// <param name="signature">The method signature.</param>
/// <returns>The method or null if failed to get it.</returns>
MMethod* GetMethod(const struct ScriptingTypeMethodSignature& signature) const;
/// <summary>
/// Returns all methods belonging to this class.
/// </summary>
@@ -271,7 +268,7 @@ public:
const Array<MField*, ArenaAllocation>& GetFields() const;
/// <summary>
/// Returns an object referencing a event with the specified name.
/// Returns an object referencing an event with the specified name.
/// </summary>
/// <param name="name">The event name.</param>
/// <returns>The event object.</returns>

View File

@@ -29,6 +29,7 @@ protected:
int32 _paramsCount;
mutable void* _returnType;
mutable Array<void*, InlinedAllocation<8>> _parameterTypes;
mutable uint64 _parameterOuts = 0;
void CacheSignature() const;
#else
StringAnsiView _name;

View File

@@ -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

View File

@@ -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);

Some files were not shown because too many files have changed in this diff Show More