diff --git a/Source/Editor/CustomEditors/Editors/BindableButtonEditor.cs b/Source/Editor/CustomEditors/Editors/BindableButtonEditor.cs new file mode 100644 index 000000000..f8323bb49 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/BindableButtonEditor.cs @@ -0,0 +1,96 @@ +using FlaxEditor.GUI; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.CustomEditors.Editors +{ + /// + /// Base class for custom button editors. + /// See , and . + /// + public class BindableButtonEditor : EnumEditor + { + private bool _isListeningForInput; + private Button _button; + + /// + /// Where or not we are currently listening for any input. + /// + protected bool IsListeningForInput + { + get => _isListeningForInput; + set + { + _isListeningForInput = value; + if (_isListeningForInput) + SetupButton(); + else + ResetButton(); + } + } + + /// + /// The window this editor is attached to. + /// Useful to hook into for key pressed, mouse buttons etc. + /// + protected Window Window { get; private set; } + + /// + public override void Initialize(LayoutElementsContainer layout) + { + Window = layout.Control.RootWindow.Window; + + var panel = layout.CustomContainer(); + panel.CustomControl.SlotsHorizontally = 2; + panel.CustomControl.SlotsVertically = 1; + + var button = panel.Button("Listen", "Press to listen for input events"); + _button = button.Button; + _button.Clicked += OnButtonClicked; + ResetButton(); + + var padding = panel.CustomControl.SlotPadding; + panel.CustomControl.Height = ComboBox.DefaultHeight + padding.Height; + + base.Initialize(panel); + } + + /// + protected override void Deinitialize() + { + _button.Clicked -= OnButtonClicked; + base.Deinitialize(); + } + + private void ResetButton() + { + _button.Text = "Listen"; + _button.BorderThickness = 1; + + var style = FlaxEngine.GUI.Style.Current; + _button.BorderColor = style.BorderNormal; + _button.BorderColorHighlighted = style.BorderHighlighted; + _button.BorderColorSelected = style.BorderSelected; + } + + private void SetupButton() + { + _button.Text = "Listening..."; + _button.BorderThickness = 2; + + var color = FlaxEngine.GUI.Style.Current.ProgressNormal; + _button.BorderColor = color; + _button.BorderColorHighlighted = color; + _button.BorderColorSelected = color; + } + + private void OnButtonClicked() + { + _isListeningForInput = !_isListeningForInput; + if (_isListeningForInput) + SetupButton(); + else + ResetButton(); + } + } +} diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 919da4301..00322fc81 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -3,9 +3,9 @@ using System; using System.Collections; using System.Linq; -using FlaxEditor.Content; -using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; +using FlaxEditor.GUI.Input; +using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Drag; using FlaxEditor.SceneGraph; @@ -54,8 +54,13 @@ namespace FlaxEditor.CustomEditors.Editors private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor) { - menu.AddSeparator(); + menu.ItemsContainer.RemoveChildren(); + menu.AddButton("Copy", linkedEditor.Copy); + var paste = menu.AddButton("Paste", linkedEditor.Paste); + paste.Enabled = linkedEditor.CanPaste; + + menu.AddSeparator(); var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked); moveUpButton.Enabled = Index > 0; @@ -65,17 +70,100 @@ namespace FlaxEditor.CustomEditors.Editors menu.AddButton("Remove", OnRemoveClicked); } - private void OnMoveUpClicked(ContextMenuButton button) + private void OnMoveUpClicked() { Editor.Move(Index, Index - 1); } - private void OnMoveDownClicked(ContextMenuButton button) + private void OnMoveDownClicked() { Editor.Move(Index, Index + 1); } - private void OnRemoveClicked(ContextMenuButton button) + private void OnRemoveClicked() + { + Editor.Remove(Index); + } + } + + private class CollectionDropPanel : DropPanel + { + /// + /// The collection editor. + /// + public CollectionEditor Editor; + + /// + /// The index of the item (zero-based). + /// + public int Index { get; private set; } + + /// + /// The linked editor. + /// + public CustomEditor LinkedEditor; + + private bool _canReorder = true; + + public void Setup(CollectionEditor editor, int index, bool canReorder = true) + { + HeaderHeight = 18; + _canReorder = canReorder; + EnableDropDownIcon = true; + var icons = FlaxEditor.Editor.Instance.Icons; + ArrowImageClosed = new SpriteBrush(icons.ArrowRight12); + ArrowImageOpened = new SpriteBrush(icons.ArrowDown12); + HeaderText = $"Element {index}"; + IsClosed = false; + Editor = editor; + Index = index; + Offsets = new Margin(7, 7, 0, 0); + + MouseButtonRightClicked += OnMouseButtonRightClicked; + if (_canReorder) + { + // TODO: Drag drop + } + } + + private void OnMouseButtonRightClicked(DropPanel panel, Float2 location) + { + if (LinkedEditor == null) + return; + var linkedEditor = LinkedEditor; + var menu = new ContextMenu(); + + menu.AddButton("Copy", linkedEditor.Copy); + var paste = menu.AddButton("Paste", linkedEditor.Paste); + paste.Enabled = linkedEditor.CanPaste; + + if (_canReorder) + { + menu.AddSeparator(); + + var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked); + moveUpButton.Enabled = Index > 0; + + var moveDownButton = menu.AddButton("Move down", OnMoveDownClicked); + moveDownButton.Enabled = Index + 1 < Editor.Count; + } + + menu.AddButton("Remove", OnRemoveClicked); + + menu.Show(panel, location); + } + + private void OnMoveUpClicked() + { + Editor.Move(Index, Index - 1); + } + + private void OnMoveDownClicked() + { + Editor.Move(Index, Index + 1); + } + + private void OnRemoveClicked() { Editor.Remove(Index); } @@ -86,12 +174,12 @@ namespace FlaxEditor.CustomEditors.Editors /// protected bool NotNullItems; - private IntegerValueElement _size; - private PropertyNameLabel _sizeLabel; + private IntValueBox _sizeBox; private Color _background; private int _elementsCount; private bool _readOnly; private bool _canReorderItems; + private CollectionAttribute.DisplayType _displayType; /// /// Gets the length of the collection. @@ -124,12 +212,13 @@ namespace FlaxEditor.CustomEditors.Editors _readOnly = false; _canReorderItems = true; _background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor; + _displayType = CollectionAttribute.DisplayType.Header; NotNullItems = false; // Try get CollectionAttribute for collection editor meta var attributes = Values.GetAttributes(); Type overrideEditorType = null; - float spacing = 10.0f; + float spacing = 1.0f; var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute); if (collection != null) { @@ -140,6 +229,7 @@ namespace FlaxEditor.CustomEditors.Editors _background = collection.BackgroundColor.Value; overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type; spacing = collection.Spacing; + _displayType = collection.Display; } var dragArea = layout.CustomContainer(); @@ -172,76 +262,77 @@ namespace FlaxEditor.CustomEditors.Editors } // Size - if (_readOnly || (NotNullItems && size == 0)) + if (layout.ContainerControl is DropPanel dropPanel) { - dragArea.Label("Size", size.ToString()); - } - else - { - var sizeProperty = dragArea.AddPropertyItem("Size"); - _sizeLabel = sizeProperty.Labels.Last(); - _size = sizeProperty.IntegerValue(); - _size.IntValue.MinValue = 0; - _size.IntValue.MaxValue = ushort.MaxValue; - _size.IntValue.Value = size; - _size.IntValue.EditEnd += OnSizeChanged; + var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height; + var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top; + _sizeBox = new IntValueBox(size) + { + MinValue = 0, + MaxValue = ushort.MaxValue, + AnchorPreset = AnchorPresets.TopRight, + Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height), + Parent = dropPanel, + }; + + var label = new Label + { + Text = "Size", + AnchorPreset = AnchorPresets.TopRight, + Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height), + Parent = dropPanel + }; + + if (_readOnly || (NotNullItems && size == 0)) + { + _sizeBox.IsReadOnly = true; + _sizeBox.Enabled = false; + } + else + { + _sizeBox.EditEnd += OnSizeChanged; + } } // Elements if (size > 0) { var panel = dragArea.VerticalPanel(); + panel.Panel.Offsets = new Margin(7, 7, 0, 0); panel.Panel.BackgroundColor = _background; var elementType = ElementType; + bool single = elementType.IsPrimitive || + elementType.Equals(new ScriptType(typeof(string))) || + elementType.IsEnum || + (elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) || + (elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) || + elementType.Equals(new ScriptType(typeof(JsonAsset))) || + elementType.Equals(new ScriptType(typeof(SettingsBase))); - // Use separate layout cells for each collection items to improve layout updates for them in separation - var useSharedLayout = elementType.IsPrimitive || elementType.IsEnum; - - if (_canReorderItems) + for (int i = 0; i < size; i++) { - for (int i = 0; i < size; i++) - { - if (i != 0 && spacing > 0f) - { - if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement) - { - if (propertiesListElement.Labels.Count > 0) - { - var label = propertiesListElement.Labels[propertiesListElement.Labels.Count - 1]; - var margin = label.Margin; - margin.Bottom += spacing; - label.Margin = margin; - } - propertiesListElement.Space(spacing); - } - else - { - panel.Space(spacing); - } - } + // Apply spacing + if (i > 0 && i < size && spacing > 0 && !single) + panel.Space(spacing); - var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null; - var property = panel.AddPropertyItem(new CollectionItemLabel(this, i)); - var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel(); - itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); + var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null; + if (_displayType == CollectionAttribute.DisplayType.Inline || (collection == null && single) || (_displayType == CollectionAttribute.DisplayType.Default && single)) + { + PropertyNameLabel itemLabel; + if (_canReorderItems) + itemLabel = new CollectionItemLabel(this, i); + else + itemLabel = new PropertyNameLabel("Element " + i); + var property = panel.AddPropertyItem(itemLabel); + var itemLayout = (LayoutElementsContainer)property; + itemLabel.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); } - } - else - { - for (int i = 0; i < size; i++) + else if (_displayType == CollectionAttribute.DisplayType.Header || (_displayType == CollectionAttribute.DisplayType.Default && !single)) { - if (i != 0 && spacing > 0f) - { - if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement) - propertiesListElement.Space(spacing); - else - panel.Space(spacing); - } - - var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null; - var property = panel.AddPropertyItem("Element " + i); - var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel(); - itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); + var cdp = panel.CustomContainer(); + cdp.CustomControl.Setup(this, i, _canReorderItems); + var itemLayout = cdp.VerticalPanel(); + cdp.CustomControl.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); } } } @@ -283,8 +374,7 @@ namespace FlaxEditor.CustomEditors.Editors /// protected override void Deinitialize() { - _size = null; - _sizeLabel = null; + _sizeBox = null; base.Deinitialize(); } @@ -311,7 +401,8 @@ namespace FlaxEditor.CustomEditors.Editors { if (IsSetBlocked) return; - Resize(_size.IntValue.Value); + + Resize(_sizeBox.Value); } /// @@ -384,14 +475,14 @@ namespace FlaxEditor.CustomEditors.Editors return; // Update reference/default value indicator - if (_sizeLabel != null) + if (_sizeBox != null) { var color = Color.Transparent; if (Values.HasReferenceValue && Values.ReferenceValue is IList referenceValue && referenceValue.Count != Count) color = FlaxEngine.GUI.Style.Current.BackgroundSelected; else if (Values.HasDefaultValue && Values.DefaultValue is IList defaultValue && defaultValue.Count != Count) color = Color.Yellow * 0.8f; - _sizeLabel.HighlightStripColor = color; + _sizeBox.BorderColor = color; } // Check if collection has been resized (by UI or from external source) diff --git a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs index 73089ff04..774f9b6b3 100644 --- a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs @@ -7,6 +7,7 @@ using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Input; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; @@ -149,13 +150,14 @@ namespace FlaxEditor.CustomEditors.Editors } } - private IntegerValueElement _size; + private IntValueBox _sizeBox; private Color _background; private int _elementsCount; private bool _readOnly; private bool _notNullItems; private bool _canEditKeys; private bool _keyEdited; + private CollectionAttribute.DisplayType _displayType; /// /// Gets the length of the collection. @@ -178,6 +180,7 @@ namespace FlaxEditor.CustomEditors.Editors _background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor; _readOnly = false; _notNullItems = false; + _displayType = CollectionAttribute.DisplayType.Header; // Try get CollectionAttribute for collection editor meta var attributes = Values.GetAttributes(); @@ -192,20 +195,40 @@ namespace FlaxEditor.CustomEditors.Editors _background = collection.BackgroundColor.Value; overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type; spacing = collection.Spacing; + _displayType = collection.Display; } // Size - if (_readOnly || !_canEditKeys) + if (layout.ContainerControl is DropPanel dropPanel) { - layout.Label("Size", size.ToString()); - } - else - { - _size = layout.IntegerValue("Size"); - _size.IntValue.MinValue = 0; - _size.IntValue.MaxValue = _notNullItems ? size : ushort.MaxValue; - _size.IntValue.Value = size; - _size.IntValue.EditEnd += OnSizeChanged; + var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height; + var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top; + _sizeBox = new IntValueBox(size) + { + MinValue = 0, + MaxValue = _notNullItems ? size : ushort.MaxValue, + AnchorPreset = AnchorPresets.TopRight, + Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height), + Parent = dropPanel, + }; + + var label = new Label + { + Text = "Size", + AnchorPreset = AnchorPresets.TopRight, + Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height), + Parent = dropPanel + }; + + if (_readOnly || !_canEditKeys) + { + _sizeBox.IsReadOnly = true; + _sizeBox.Enabled = false; + } + else + { + _sizeBox.EditEnd += OnSizeChanged; + } } // Elements @@ -216,29 +239,23 @@ namespace FlaxEditor.CustomEditors.Editors var keysEnumerable = ((IDictionary)Values[0]).Keys.OfType(); var keys = keysEnumerable as object[] ?? keysEnumerable.ToArray(); var valuesType = new ScriptType(valueType); + + bool single = valuesType.IsPrimitive || + valuesType.Equals(new ScriptType(typeof(string))) || + valuesType.IsEnum || + (valuesType.GetFields().Length == 1 && valuesType.GetProperties().Length == 0) || + (valuesType.GetProperties().Length == 1 && valuesType.GetFields().Length == 0) || + valuesType.Equals(new ScriptType(typeof(JsonAsset))) || + valuesType.Equals(new ScriptType(typeof(SettingsBase))); // Use separate layout cells for each collection items to improve layout updates for them in separation var useSharedLayout = valueType.IsPrimitive || valueType.IsEnum; for (int i = 0; i < size; i++) { - if (i != 0 && spacing > 0f) + if (i > 0 && i < size && spacing > 0) { - if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement) - { - if (propertiesListElement.Labels.Count > 0) - { - var label = propertiesListElement.Labels[propertiesListElement.Labels.Count - 1]; - var margin = label.Margin; - margin.Bottom += spacing; - label.Margin = margin; - } - propertiesListElement.Space(spacing); - } - else - { - panel.Space(spacing); - } + panel.Space(spacing); } var key = keys.ElementAt(i); @@ -310,7 +327,7 @@ namespace FlaxEditor.CustomEditors.Editors if (IsSetBlocked) return; - Resize(_size.IntValue.Value); + Resize(_sizeBox.Value); } /// diff --git a/Source/Editor/CustomEditors/Editors/GamepadButtonEditor.cs b/Source/Editor/CustomEditors/Editors/GamepadButtonEditor.cs new file mode 100644 index 000000000..6b0d190c0 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/GamepadButtonEditor.cs @@ -0,0 +1,51 @@ +using System; +using FlaxEngine; + +namespace FlaxEditor.CustomEditors.Editors +{ + /// + /// Custom editor for . + /// Allows capturing gamepad buttons and assigning them + /// to the edited value. + /// + [CustomEditor(typeof(GamepadButton))] + public class GamepadButtonEditor : BindableButtonEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + FlaxEngine.Scripting.Update += OnUpdate; + } + + /// + protected override void Deinitialize() + { + FlaxEngine.Scripting.Update -= OnUpdate; + base.Deinitialize(); + } + + private void OnUpdate() + { + if (!IsListeningForInput) + return; + + // Since there is no way to get an event about + // which gamepad pressed what button, we have + // to poll all gamepads and buttons manually. + for (var i = 0; i < Input.GamepadsCount; i++) + { + var pad = Input.Gamepads[i]; + foreach (var btn in Enum.GetValues()) + { + if (pad.GetButtonUp(btn)) + { + IsListeningForInput = false; + SetValue(btn); + return; + } + } + } + } + } +} diff --git a/Source/Editor/CustomEditors/Editors/KeyboardKeysEditor.cs b/Source/Editor/CustomEditors/Editors/KeyboardKeysEditor.cs new file mode 100644 index 000000000..375ea1d96 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/KeyboardKeysEditor.cs @@ -0,0 +1,36 @@ +using FlaxEngine; + +namespace FlaxEditor.CustomEditors.Editors +{ + /// + /// Custom editor for . + /// Allows capturing key presses and assigning them + /// to the edited value. + /// + [CustomEditor(typeof(KeyboardKeys))] + public class KeyboardKeysEditor : BindableButtonEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + Window.KeyUp += OnKeyUp; + } + + /// + protected override void Deinitialize() + { + Window.KeyUp -= OnKeyUp; + base.Deinitialize(); + } + + private void OnKeyUp(KeyboardKeys key) + { + if (!IsListeningForInput) + return; + IsListeningForInput = false; + + SetValue(key); + } + } +} diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index f901b20d9..f9ccf548a 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -4,6 +4,7 @@ using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.Scripting; using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Editors { @@ -13,7 +14,7 @@ namespace FlaxEditor.CustomEditors.Editors [CustomEditor(typeof(ModelInstanceEntry)), DefaultEditor] public sealed class ModelInstanceEntryEditor : GenericEditor { - private GroupElement _group; + private DropPanel _mainPanel; private bool _updateName; private int _entryIndex; private bool _isRefreshing; @@ -25,8 +26,11 @@ namespace FlaxEditor.CustomEditors.Editors public override void Initialize(LayoutElementsContainer layout) { _updateName = true; - var group = layout.Group("Entry"); - _group = group; + if (layout.ContainerControl.Parent is DropPanel panel) + { + _mainPanel = panel; + _mainPanel.HeaderText = "Entry"; + } if (ParentEditor == null || HasDifferentTypes) return; @@ -60,19 +64,19 @@ namespace FlaxEditor.CustomEditors.Editors var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase); for (var i = 1; i < parentEditorValues.Count; i++) materialValue.Add(_material); - var materialEditor = (AssetRefEditor)_group.Property(materialLabel, materialValue); + var materialEditor = (AssetRefEditor)layout.Property(materialLabel, materialValue); materialEditor.Values.SetDefaultValue(defaultValue); materialEditor.RefreshDefaultValue(); materialEditor.Picker.SelectedItemChanged += OnSelectedMaterialChanged; _materialEditor = materialEditor; } - base.Initialize(group); + base.Initialize(layout); } private void OnSelectedMaterialChanged() { - if (_isRefreshing) + if (_isRefreshing || _modelInstance == null) return; _isRefreshing = true; var slots = _modelInstance.MaterialSlots; @@ -120,7 +124,7 @@ namespace FlaxEditor.CustomEditors.Editors { // Update panel title to match material slot name if (_updateName && - _group != null && + _mainPanel != null && ParentEditor?.ParentEditor != null && ParentEditor.ParentEditor.Values.Count > 0) { @@ -131,7 +135,7 @@ namespace FlaxEditor.CustomEditors.Editors if (slots != null && slots.Length > entryIndex) { _updateName = false; - _group.Panel.HeaderText = "Entry " + slots[entryIndex].Name; + _mainPanel.HeaderText = "Entry " + slots[entryIndex].Name; } } } diff --git a/Source/Editor/CustomEditors/Editors/MouseButtonEditor.cs b/Source/Editor/CustomEditors/Editors/MouseButtonEditor.cs new file mode 100644 index 000000000..4e46f6717 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/MouseButtonEditor.cs @@ -0,0 +1,37 @@ +using FlaxEngine; + +namespace FlaxEditor.CustomEditors.Editors +{ + /// + /// Custom editor for . + /// Allows capturing mouse button presses and assigning them + /// to the edited value. + /// + [CustomEditor(typeof(MouseButton))] + public class MouseButtonEditor : BindableButtonEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + Window.MouseUp += OnMouseUp; + } + + /// + protected override void Deinitialize() + { + Window.MouseUp -= OnMouseUp; + base.Deinitialize(); + } + + private void OnMouseUp(ref Float2 mouse, MouseButton button, ref bool handled) + { + if (!IsListeningForInput) + return; + IsListeningForInput = false; + + SetValue(button); + handled = true; + } + } +} diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 84f58daf1..9e8e0485e 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -75,6 +75,9 @@ namespace FlaxEditor.GUI /// protected virtual void OnSelectedItemChanged() { + if (IsDisposing) + return; + // Update tooltip string tooltip; if (Validator.SelectedItem is AssetItem assetItem) diff --git a/Source/Editor/GUI/ComboBox.cs b/Source/Editor/GUI/ComboBox.cs index 0417cc7e3..176e83374 100644 --- a/Source/Editor/GUI/ComboBox.cs +++ b/Source/Editor/GUI/ComboBox.cs @@ -428,6 +428,13 @@ namespace FlaxEditor.GUI // Show dropdown list _popupMenu.MinimumWidth = Width; _popupMenu.Show(this, new Float2(1, Height)); + + // Adjust menu position if it is not the down direction + if (_popupMenu.Direction == ContextMenuDirection.RightUp) + { + var position = _popupMenu.RootWindow.Window.Position; + _popupMenu.RootWindow.Window.Position = new Float2(position.X, position.Y - Height); + } } } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenu.cs index 25f45a1f8..072b01678 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenu.cs @@ -408,9 +408,9 @@ namespace FlaxEditor.GUI.ContextMenu { foreach (var child in _panel.Children) { - if (child is ContextMenuChildMenu item && item.Visible) + if (child is ContextMenuButton item && item.Visible) { - item.AdjustArrowAmount = -_panel.VScrollBar.Width; + item.ExtraAdjustmentAmount = -_panel.VScrollBar.Width; } } } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs index e371f7c4b..485cde1a7 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs @@ -14,6 +14,11 @@ namespace FlaxEditor.GUI.ContextMenu public class ContextMenuButton : ContextMenuItem { private bool _isMouseDown; + + /// + /// The amount to adjust the short keys and arrow image by in x coordinates. + /// + public float ExtraAdjustmentAmount = 0; /// /// Event fired when user clicks on the button. @@ -133,7 +138,7 @@ namespace FlaxEditor.GUI.ContextMenu if (!string.IsNullOrEmpty(ShortKeys)) { // Draw short keys - Render2D.DrawText(style.FontMedium, ShortKeys, textRect, textColor, TextAlignment.Far, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, ShortKeys, new Rectangle(textRect.X + ExtraAdjustmentAmount, textRect.Y, textRect.Width, textRect.Height), textColor, TextAlignment.Far, TextAlignment.Center); } // Draw icon diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs index 49a60a04e..d71166196 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs @@ -17,11 +17,6 @@ namespace FlaxEditor.GUI.ContextMenu /// public readonly ContextMenu ContextMenu = new ContextMenu(); - /// - /// The amount to adjust the arrow image by in x coordinates. - /// - public float AdjustArrowAmount = 0; - /// /// Initializes a new instance of the class. /// @@ -49,7 +44,7 @@ namespace FlaxEditor.GUI.ContextMenu // Draw arrow if (ContextMenu.HasChildren) - Render2D.DrawSprite(style.ArrowRight, new Rectangle(Width - 15 + AdjustArrowAmount, (Height - 12) / 2, 12, 12), Enabled ? isCMopened ? style.BackgroundSelected : style.Foreground : style.ForegroundDisabled); + Render2D.DrawSprite(style.ArrowRight, new Rectangle(Width - 15 + ExtraAdjustmentAmount, (Height - 12) / 2, 12, 12), Enabled ? isCMopened ? style.BackgroundSelected : style.Foreground : style.ForegroundDisabled); } /// diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs index 3a9780ed9..e86e1382a 100644 --- a/Source/Editor/GUI/Tree/Tree.cs +++ b/Source/Editor/GUI/Tree/Tree.cs @@ -19,7 +19,7 @@ namespace FlaxEditor.GUI.Tree /// /// The key updates timeout in seconds. /// - public static float KeyUpdateTimeout = 0.12f; + public static float KeyUpdateTimeout = 0.25f; /// /// Delegate for selected tree nodes collection change. @@ -113,7 +113,7 @@ namespace FlaxEditor.GUI.Tree AutoFocus = false; _supportMultiSelect = supportMultiSelect; - _keyUpdateTime = KeyUpdateTimeout * 10; + _keyUpdateTime = KeyUpdateTimeout; } internal void OnRightClickInternal(TreeNode node, ref Float2 location) @@ -347,10 +347,12 @@ namespace FlaxEditor.GUI.Tree if (ContainsFocus && node != null && node.AutoFocus) { var window = Root; + if (window.GetKeyDown(KeyboardKeys.ArrowUp) || window.GetKeyDown(KeyboardKeys.ArrowDown)) + _keyUpdateTime = KeyUpdateTimeout; if (_keyUpdateTime >= KeyUpdateTimeout && window is WindowRootControl windowRoot && windowRoot.Window.IsFocused) { - bool keyUpArrow = window.GetKeyDown(KeyboardKeys.ArrowUp); - bool keyDownArrow = window.GetKeyDown(KeyboardKeys.ArrowDown); + bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp); + bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown); // Check if arrow flags are different if (keyDownArrow != keyUpArrow) diff --git a/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs b/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs index a73c3d2ee..648ec74e3 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs @@ -45,6 +45,21 @@ namespace FlaxEditor.Modules.SourceCodeEditing _checkAssembly = checkAssembly; } + /// + /// Gets the type matching the certain Script. + /// + /// The content item. + /// The type matching that item, or null if not found. + public ScriptType Get(Content.ScriptItem script) + { + foreach (var type in Get()) + { + if (type.ContentItem == script) + return type; + } + return ScriptType.Null; + } + /// /// Gets all the types from the all loaded assemblies (including project scripts and scripts from the plugins). /// diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 9cfe5b7bd..bf39a501b 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -29,6 +29,7 @@ namespace FlaxEditor.SceneGraph.GUI private DragScripts _dragScripts; private DragAssets _dragAssets; private DragActorType _dragActorType; + private DragScriptItems _dragScriptItems; private DragHandlers _dragHandlers; private List _highlights; private bool _hasSearchFilter; @@ -395,6 +396,13 @@ namespace FlaxEditor.SceneGraph.GUI } if (_dragActorType.OnDragEnter(data)) return _dragActorType.Effect; + if (_dragScriptItems == null) + { + _dragScriptItems = new DragScriptItems(ValidateDragScriptItem); + _dragHandlers.Add(_dragScriptItems); + } + if (_dragScriptItems.OnDragEnter(data)) + return _dragScriptItems.Effect; return DragDropEffect.None; } @@ -673,7 +681,34 @@ namespace FlaxEditor.SceneGraph.GUI actor.Transform = Actor.Transform; ActorNode.Root.Spawn(actor, Actor); } + result = DragDropEffect.Move; + } + // Drag script item + else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag) + { + var spawnParent = myActor; + if (DragOverMode == DragItemPositioning.Above || DragOverMode == DragItemPositioning.Below) + spawnParent = newParent; + for (int i = 0; i < _dragScriptItems.Objects.Count; i++) + { + var item = _dragScriptItems.Objects[i]; + var actorType = Editor.Instance.CodeEditing.Actors.Get(item); + if (actorType != ScriptType.Null) + { + var actor = actorType.CreateInstance() as Actor; + if (actor == null) + { + Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName); + continue; + } + actor.StaticFlags = spawnParent.StaticFlags; + actor.Name = actorType.Name; + actor.Transform = spawnParent.Transform; + ActorNode.Root.Spawn(actor, spawnParent); + actor.OrderInParent = newOrder; + } + } result = DragDropEffect.Move; } @@ -728,6 +763,11 @@ namespace FlaxEditor.SceneGraph.GUI return true; } + private static bool ValidateDragScriptItem(ScriptItem script) + { + return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null; + } + /// protected override void DoDragDrop() { @@ -768,6 +808,7 @@ namespace FlaxEditor.SceneGraph.GUI _dragScripts = null; _dragAssets = null; _dragActorType = null; + _dragScriptItems = null; _dragHandlers?.Clear(); _dragHandlers = null; _highlights = null; diff --git a/Source/Editor/Surface/AnimGraphSurface.cs b/Source/Editor/Surface/AnimGraphSurface.cs index d7746662f..ccd78412a 100644 --- a/Source/Editor/Surface/AnimGraphSurface.cs +++ b/Source/Editor/Surface/AnimGraphSurface.cs @@ -113,6 +113,25 @@ namespace FlaxEditor.Surface ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; } + internal AnimGraphTraceEvent[] LastTraceEvents; + + internal bool TryGetTraceEvent(SurfaceNode node, out AnimGraphTraceEvent traceEvent) + { + if (LastTraceEvents != null) + { + foreach (var e in LastTraceEvents) + { + if (e.NodeId == node.ID) + { + traceEvent = e; + return true; + } + } + } + traceEvent = default; + return false; + } + private static SurfaceStyle CreateStyle() { var editor = Editor.Instance; @@ -383,6 +402,7 @@ namespace FlaxEditor.Surface } ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; _nodesCache.Wait(); + LastTraceEvents = null; base.OnDestroy(); } diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index 9d4367303..9ec1bab19 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -1583,14 +1583,24 @@ namespace FlaxEditor.Surface.Archetypes None = 0, /// - /// Transition rule will be rechecked during active transition with option to interrupt transition. + /// Transition rule will be rechecked during active transition with option to interrupt transition (to go back to the source state). /// RuleRechecking = 1, /// - /// Interrupted transition is immediately stopped without blending out. + /// Interrupted transition is immediately stopped without blending out (back to the source/destination state). /// Instant = 2, + + /// + /// Enables checking other transitions in the source state that might interrupt this one. + /// + SourceState = 4, + + /// + /// Enables checking transitions in the destination state that might interrupt this one. + /// + DestinationState = 8, } /// @@ -1613,6 +1623,8 @@ namespace FlaxEditor.Surface.Archetypes UseDefaultRule = 4, InterruptionRuleRechecking = 8, InterruptionInstant = 16, + InterruptionSourceState = 32, + InterruptionDestinationState = 64, } /// @@ -1773,7 +1785,7 @@ namespace FlaxEditor.Surface.Archetypes } /// - /// Transition interruption options. + /// Transition interruption options (flags, can select multiple values). /// [EditorOrder(70), DefaultValue(InterruptionFlags.None)] public InterruptionFlags Interruption @@ -1785,12 +1797,18 @@ namespace FlaxEditor.Surface.Archetypes flags |= InterruptionFlags.RuleRechecking; if (_data.HasFlag(Data.FlagTypes.InterruptionInstant)) flags |= InterruptionFlags.Instant; + if (_data.HasFlag(Data.FlagTypes.InterruptionSourceState)) + flags |= InterruptionFlags.SourceState; + if (_data.HasFlag(Data.FlagTypes.InterruptionDestinationState)) + flags |= InterruptionFlags.DestinationState; return flags; } set { _data.SetFlag(Data.FlagTypes.InterruptionRuleRechecking, value.HasFlag(InterruptionFlags.RuleRechecking)); _data.SetFlag(Data.FlagTypes.InterruptionInstant, value.HasFlag(InterruptionFlags.Instant)); + _data.SetFlag(Data.FlagTypes.InterruptionSourceState, value.HasFlag(InterruptionFlags.SourceState)); + _data.SetFlag(Data.FlagTypes.InterruptionDestinationState, value.HasFlag(InterruptionFlags.DestinationState)); SourceState.SaveTransitions(true); } } diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index 446883406..cbe9c3d85 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -28,6 +28,51 @@ namespace FlaxEditor.Surface.Archetypes } } + /// + /// Customized for Blend with Mask node. + /// + public class SkeletonMaskSample : SurfaceNode + { + private AssetSelect _assetSelect; + private Box _assetBox; + + /// + public SkeletonMaskSample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + /// + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + base.OnSurfaceLoaded(action); + + if (Surface != null) + { + _assetSelect = GetChild(); + + // 4 is the id of skeleton mask parameter node. + if (TryGetBox(4, out var box)) + { + _assetBox = box; + _assetSelect.Visible = !_assetBox.HasAnyConnection; + } + } + } + + /// + public override void ConnectionTick(Box box) + { + base.ConnectionTick(box); + + if (_assetBox == null) + return; + if (box.ID != _assetBox.ID) + return; + _assetSelect.Visible = !box.HasAnyConnection; + } + } + /// /// Customized for the animation sampling nodes /// @@ -36,6 +81,7 @@ namespace FlaxEditor.Surface.Archetypes { private AssetSelect _assetSelect; private Box _assetBox; + private ProgressBar _playbackPos; /// public Sample(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) @@ -75,7 +121,7 @@ namespace FlaxEditor.Surface.Archetypes Title = _assetBox.HasAnyConnection || asset == null ? "Animation" : asset.ShortName; else Title = asset?.ShortName ?? "Animation"; - + var style = Style.Current; Resize(Mathf.Max(230, style.FontLarge.MeasureText(Title).X + 30), 160); } @@ -93,6 +139,36 @@ namespace FlaxEditor.Surface.Archetypes _assetSelect.Visible = !box.HasAnyConnection; UpdateTitle(); } + + /// + public override void Update(float deltaTime) + { + // Debug current playback position + if (((AnimGraphSurface)Surface).TryGetTraceEvent(this, out var traceEvent) && traceEvent.Asset is FlaxEngine.Animation anim) + { + if (_playbackPos == null) + { + _playbackPos = new ProgressBar + { + SmoothingScale = 0.0f, + Offsets = Margin.Zero, + AnchorPreset = AnchorPresets.HorizontalStretchBottom, + Parent = this, + Height = 12.0f, + }; + _playbackPos.Y -= 16.0f; + } + _playbackPos.Visible = true; + _playbackPos.Maximum = anim.Duration; + _playbackPos.Value = traceEvent.Value; // AnimGraph reports position in animation frames, not time + } + else if (_playbackPos != null) + { + _playbackPos.Visible = false; + } + + base.Update(deltaTime); + } } /// @@ -493,7 +569,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 10, Title = "Blend Additive", - Description = + Description = "Blend animation poses (with additive mode)" + "\n" + "\nNote: " + @@ -521,6 +597,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 11, Title = "Blend with Mask", Description = "Blend animation poses using skeleton mask", + Create = (id, context, arch, groupArch) => new SkeletonMaskSample(id, context, arch, groupArch), Flags = NodeFlags.AnimGraph, Size = new Float2(180, 140), DefaultValues = new object[] @@ -534,7 +611,8 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(0, "Pose A", true, typeof(void), 1), NodeElementArchetype.Factory.Input(1, "Pose B", true, typeof(void), 2), NodeElementArchetype.Factory.Input(2, "Alpha", true, typeof(float), 3, 0), - NodeElementArchetype.Factory.Asset(0, 70, 1, typeof(SkeletonMask)), + NodeElementArchetype.Factory.Input(3, "Skeleton Mask Asset", true, typeof(SkeletonMask), 4), + NodeElementArchetype.Factory.Asset(0, Surface.Constants.LayoutOffsetY * 4, 1, typeof(SkeletonMask)), } }, new NodeArchetype diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 53950dad2..e8b92d121 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -763,6 +763,17 @@ namespace FlaxEditor.Surface.Archetypes public string Name; public ScriptType Type; public bool IsOut; + + public override string ToString() + { + var sb = new StringBuilder(); + if (IsOut) + sb.Append("out "); + sb.Append(Type.ToString()); + sb.Append(" "); + sb.Append(Name); + return sb.ToString(); + } } private struct SignatureInfo @@ -892,7 +903,7 @@ namespace FlaxEditor.Surface.Archetypes { ref var param = ref signature.Params[i]; ref var paramMember = ref memberParameters[i]; - if (param.Type != paramMember.Type || param.IsOut != paramMember.IsOut) + if (!SurfaceUtils.AreScriptTypesEqual(param.Type, paramMember.Type) || param.IsOut != paramMember.IsOut) { // Special case: param.Type is serialized as just a type while paramMember.Type might be a reference for output parameters (eg. `out Int32` vs `out Int32&`) var paramMemberTypeName = paramMember.Type.TypeName; @@ -1660,7 +1671,7 @@ namespace FlaxEditor.Surface.Archetypes SaveSignature(); // Check if return type has been changed - if (_signature.ReturnType != prevReturnType) + if (!SurfaceUtils.AreScriptTypesEqual(_signature.ReturnType, prevReturnType)) { // Update all return nodes used by this function to match the new type var usedNodes = DepthFirstTraversal(false); @@ -2158,7 +2169,7 @@ namespace FlaxEditor.Surface.Archetypes return false; for (int i = 0; i < _signature.Length; i++) { - if (_signature[i].Type != sig.Parameters[i].Type) + if (!SurfaceUtils.AreScriptTypesEqual(_signature[i].Type, sig.Parameters[i].Type)) return false; } return true; diff --git a/Source/Editor/Surface/MaterialSurface.cs b/Source/Editor/Surface/MaterialSurface.cs index ba7f2e01e..a7fa6cb38 100644 --- a/Source/Editor/Surface/MaterialSurface.cs +++ b/Source/Editor/Surface/MaterialSurface.cs @@ -105,7 +105,7 @@ namespace FlaxEditor.Surface if (node != null) { - args.SurfaceLocation.X += node.Width + 10; + args.SurfaceLocation.Y += node.Height + 10; } } diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 5f4c3ef07..2c8448e92 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -532,5 +532,24 @@ namespace FlaxEditor.Surface value = new Double4(i); return value; } + + private static bool AreScriptTypesEqualInner(ScriptType left, ScriptType right) + { + // Special case for Vector types that use typedefs and might overlap + if (left.Type == typeof(Vector2) && (right.Type == typeof(Float2) || right.Type == typeof(Double2))) + return true; + if (left.Type == typeof(Vector3) && (right.Type == typeof(Float3) || right.Type == typeof(Double3))) + return true; + if (left.Type == typeof(Vector4) && (right.Type == typeof(Float4) || right.Type == typeof(Double4))) + return true; + return false; + } + + internal static bool AreScriptTypesEqual(ScriptType left, ScriptType right) + { + if (left == right) + return true; + return AreScriptTypesEqualInner(left, right) || AreScriptTypesEqualInner(right, left); + } } } diff --git a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs index 72b9242a4..07118228e 100644 --- a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs +++ b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs @@ -24,6 +24,8 @@ namespace FlaxEditor.Surface.Undo public ConnectBoxesAction(InputBox iB, OutputBox oB, bool connect) { + if (iB == null || oB == null || iB.ParentNode == null || oB.ParentNode == null) + throw new System.ArgumentNullException(); _surface = iB.Surface; _context = new ContextHandle(iB.ParentNode.Context); _connect = connect; diff --git a/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs b/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs index 610a336b7..bf80eb34d 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/FlattenMode.cs @@ -18,7 +18,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public float TargetHeight = 0.0f; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { // If used with invert mode pick the target height level if (p.Options.Invert) diff --git a/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs b/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs index d4c10f00e..3d75a56f2 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/HolesMode.cs @@ -26,7 +26,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt } /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { var strength = p.Strength * -10.0f; var brushPosition = p.Gizmo.CursorPosition; diff --git a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs index 9a42d2ae8..cce2f982f 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; using FlaxEditor.Tools.Terrain.Brushes; using FlaxEngine; @@ -50,18 +51,20 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public virtual bool EditHoles => false; /// - /// Applies the modification to the terrain. + /// Gets all patches that will be affected by the brush /// /// The brush. /// The options. /// The gizmo. /// The terrain. - public unsafe void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) + public virtual unsafe List GetAffectedPatches(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) { + List affectedPatches = new(); + // Combine final apply strength float strength = Strength * options.Strength * options.DeltaTime; if (strength <= 0.0f) - return; + return affectedPatches; if (options.Invert && SupportsNegativeApply) strength *= -1; @@ -72,20 +75,10 @@ namespace FlaxEditor.Tools.Terrain.Sculpt var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount; var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer(); var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; - ApplyParams p = new ApplyParams - { - Terrain = terrain, - Brush = brush, - Gizmo = gizmo, - Options = options, - Strength = strength, - HeightmapSize = heightmapSize, - TempBuffer = tempBuffer, - }; // Get brush bounds in terrain local space var brushBounds = gizmo.CursorBrushBounds; - terrain.GetLocalToWorldMatrix(out p.TerrainWorld); + terrain.GetLocalToWorldMatrix(out var terrainWorld); terrain.GetWorldToLocalMatrix(out var terrainInvWorld); BoundingBox.Transform(ref brushBounds, ref terrainInvWorld, out var brushBoundsLocal); @@ -131,26 +124,78 @@ namespace FlaxEditor.Tools.Terrain.Sculpt if (sourceHeights == null && sourceHoles == null) throw new Exception("Cannot modify terrain. Loading heightmap failed. See log for more info."); - // Record patch data before editing it - if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord)) + ApplyParams p = new ApplyParams { - gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord); - } + Terrain = terrain, + TerrainWorld = terrainWorld, + Brush = brush, + Gizmo = gizmo, + Options = options, + Strength = strength, + HeightmapSize = heightmapSize, + TempBuffer = tempBuffer, + ModifiedOffset = modifiedOffset, + ModifiedSize = modifiedSize, + PatchCoord = patch.PatchCoord, + PatchPositionLocal = patchPositionLocal, + SourceHeightMap = sourceHeights, + SourceHolesMask = sourceHoles, + }; - // Apply modification - p.ModifiedOffset = modifiedOffset; - p.ModifiedSize = modifiedSize; - p.PatchCoord = patch.PatchCoord; - p.PatchPositionLocal = patchPositionLocal; - p.SourceHeightMap = sourceHeights; - p.SourceHolesMask = sourceHoles; - Apply(ref p); + affectedPatches.Add(p); } + return affectedPatches; + } + + /// + /// Applies the modification to the terrain. + /// + /// The brush. + /// The options. + /// The gizmo. + /// The terrain. + public void Apply(Brush brush, ref Options options, SculptTerrainGizmoMode gizmo, FlaxEngine.Terrain terrain) + { + var affectedPatches = GetAffectedPatches(brush, ref options, gizmo, terrain); + + if (affectedPatches.Count == 0) + { + return; + } + + ApplyBrush(gizmo, affectedPatches); + // Auto NavMesh rebuild + var brushBounds = gizmo.CursorBrushBounds; gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds); } + /// + /// Applies the brush to all affected patches + /// + /// + /// + public virtual void ApplyBrush(SculptTerrainGizmoMode gizmo, List affectedPatches) + { + for (int i = 0; i < affectedPatches.Count; i++) + { + ApplyParams patchApplyParams = affectedPatches[i]; + + // Record patch data before editing it + if (!gizmo.CurrentEditUndoAction.HashPatch(ref patchApplyParams.PatchCoord)) + { + gizmo.CurrentEditUndoAction.AddPatch(ref patchApplyParams.PatchCoord); + } + + ApplyBrushToPatch(ref patchApplyParams); + + // Auto NavMesh rebuild + var brushBounds = gizmo.CursorBrushBounds; + gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds); + } + } + /// /// The mode apply parameters. /// @@ -231,6 +276,6 @@ namespace FlaxEditor.Tools.Terrain.Sculpt /// Applies the modification to the terrain. /// /// The parameters to use. - public abstract void Apply(ref ApplyParams p); + public abstract void ApplyBrushToPatch(ref ApplyParams p); } } diff --git a/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs b/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs index 32d02bb3f..7fcb09288 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/NoiseMode.cs @@ -29,7 +29,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public override bool SupportsNegativeApply => true; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { // Prepare var brushPosition = p.Gizmo.CursorPosition; diff --git a/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs b/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs index c3409f30c..381b39ebd 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/SculptMode.cs @@ -15,7 +15,7 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public override bool SupportsNegativeApply => true; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) { var strength = p.Strength * 1000.0f; var brushPosition = p.Gizmo.CursorPosition; diff --git a/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs b/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs index d23eae8dd..387bb555e 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/SmoothMode.cs @@ -1,7 +1,8 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. -using System; +using System.Collections.Generic; using FlaxEngine; +using System; namespace FlaxEditor.Tools.Terrain.Sculpt { @@ -19,43 +20,109 @@ namespace FlaxEditor.Tools.Terrain.Sculpt public float FilterRadius = 0.4f; /// - public override unsafe void Apply(ref ApplyParams p) + public override unsafe void ApplyBrush(SculptTerrainGizmoMode gizmo, List affectedPatches) { - // Prepare - var brushPosition = p.Gizmo.CursorPosition; - var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * p.Brush.Size), 2); - var max = p.HeightmapSize - 1; - var strength = Mathf.Saturate(p.Strength); - - // Apply brush modification Profiler.BeginEvent("Apply Brush"); - for (int z = 0; z < p.ModifiedSize.Y; z++) + + // TODO: don't need these on each patch; just need them once + var heightmapSize = affectedPatches[0].HeightmapSize; + var radius = Mathf.Max(Mathf.CeilToInt(FilterRadius * 0.01f * affectedPatches[0].Brush.Size), 2); + + // Calculate bounding coordinates of the total affected area + Int2 modifieedAreaMinCoord = Int2.Maximum; + Int2 modifiedAreaMaxCoord = Int2.Minimum; + for (int i = 0; i < affectedPatches.Count; i++) { - var zz = z + p.ModifiedOffset.Y; - for (int x = 0; x < p.ModifiedSize.X; x++) + var patch = affectedPatches[i]; + + var tl = (patch.PatchCoord * (heightmapSize - 1)) + patch.ModifiedOffset; + var br = tl + patch.ModifiedSize; + + if (tl.X <= modifieedAreaMinCoord.X && tl.Y <= modifieedAreaMinCoord.Y) + modifieedAreaMinCoord = tl; + if (br.X >= modifiedAreaMaxCoord.X && br.Y >= modifiedAreaMaxCoord.Y) + modifiedAreaMaxCoord = br; + } + var totalModifiedSize = modifiedAreaMaxCoord - modifieedAreaMinCoord; + + // Build map of heights in affected area + var modifiedHeights = new float[totalModifiedSize.X * totalModifiedSize.Y]; + for (int i = 0; i < affectedPatches.Count; i++) + { + var patch = affectedPatches[i]; + + for (int z = 0; z < patch.ModifiedSize.Y; z++) { - var xx = x + p.ModifiedOffset.X; - var sourceHeight = p.SourceHeightMap[zz * p.HeightmapSize + xx]; - - var samplePositionLocal = p.PatchPositionLocal + new Vector3(xx * FlaxEngine.Terrain.UnitsPerVertex, sourceHeight, zz * FlaxEngine.Terrain.UnitsPerVertex); - Vector3.Transform(ref samplePositionLocal, ref p.TerrainWorld, out Vector3 samplePositionWorld); - - var paintAmount = p.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength; - - if (paintAmount > 0) + for (int x = 0; x < patch.ModifiedSize.X; x++) { + // Read height from current patch + var localCoordX = (x + patch.ModifiedOffset.X); + var localCoordY = (z + patch.ModifiedOffset.Y); + var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX]; + + // Calculate the absolute coordinate of the terrain point + var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z); + var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord; + + // Store height + var index = (currentPointCoordRelativeToModifiedArea.Y * totalModifiedSize.X) + currentPointCoordRelativeToModifiedArea.X; + modifiedHeights[index] = coordHeight; + } + } + } + + // Iterate through modified points and smooth now that we have height information for all necessary points + for (int i = 0; i < affectedPatches.Count; i++) + { + var patch = affectedPatches[i]; + var brushPosition = patch.Gizmo.CursorPosition; + var strength = Mathf.Saturate(patch.Strength); + + for (int z = 0; z < patch.ModifiedSize.Y; z++) + { + for (int x = 0; x < patch.ModifiedSize.X; x++) + { + // Read height from current patch + var localCoordX = (x + patch.ModifiedOffset.X); + var localCoordY = (z + patch.ModifiedOffset.Y); + var coordHeight = patch.SourceHeightMap[(localCoordY * heightmapSize) + localCoordX]; + + // Calculate the absolute coordinate of the terrain point + var absoluteCurrentPointCoord = patch.PatchCoord * (heightmapSize - 1) + patch.ModifiedOffset + new Int2(x, z); + var currentPointCoordRelativeToModifiedArea = absoluteCurrentPointCoord - modifieedAreaMinCoord; + + // Calculate brush influence at the current position + var samplePositionLocal = patch.PatchPositionLocal + new Vector3(localCoordX * FlaxEngine.Terrain.UnitsPerVertex, coordHeight, localCoordY * FlaxEngine.Terrain.UnitsPerVertex); + Vector3.Transform(ref samplePositionLocal, ref patch.TerrainWorld, out Vector3 samplePositionWorld); + var paintAmount = patch.Brush.Sample(ref brushPosition, ref samplePositionWorld) * strength; + + if (paintAmount == 0) + { + patch.TempBuffer[z * patch.ModifiedSize.X + x] = coordHeight; + continue; + } + + // Record patch data before editing it + if (!gizmo.CurrentEditUndoAction.HashPatch(ref patch.PatchCoord)) + { + gizmo.CurrentEditUndoAction.AddPatch(ref patch.PatchCoord); + } + // Sum the nearby values float smoothValue = 0; int smoothValueSamples = 0; - int minX = Math.Max(x - radius + p.ModifiedOffset.X, 0); - int minZ = Math.Max(z - radius + p.ModifiedOffset.Y, 0); - int maxX = Math.Min(x + radius + p.ModifiedOffset.X, max); - int maxZ = Math.Min(z + radius + p.ModifiedOffset.Y, max); + + var minX = Math.Max(0, currentPointCoordRelativeToModifiedArea.X - radius); + var maxX = Math.Min(totalModifiedSize.X - 1, currentPointCoordRelativeToModifiedArea.X + radius); + var minZ = Math.Max(0, currentPointCoordRelativeToModifiedArea.Y - radius); + var maxZ = Math.Min(totalModifiedSize.Y - 1, currentPointCoordRelativeToModifiedArea.Y + radius); + for (int dz = minZ; dz <= maxZ; dz++) { for (int dx = minX; dx <= maxX; dx++) { - var height = p.SourceHeightMap[dz * p.HeightmapSize + dx]; + var coordIndex = (dz * totalModifiedSize.X) + dx; + var height = modifiedHeights[coordIndex]; smoothValue += height; smoothValueSamples++; } @@ -65,18 +132,26 @@ namespace FlaxEditor.Tools.Terrain.Sculpt smoothValue /= smoothValueSamples; // Blend between the height and smooth value - p.TempBuffer[z * p.ModifiedSize.X + x] = Mathf.Lerp(sourceHeight, smoothValue, paintAmount); - } - else - { - p.TempBuffer[z * p.ModifiedSize.X + x] = sourceHeight; + var newHeight = Mathf.Lerp(coordHeight, smoothValue, paintAmount); + patch.TempBuffer[z * patch.ModifiedSize.X + x] = newHeight; } } - } - Profiler.EndEvent(); - // Update terrain patch - TerrainTools.ModifyHeightMap(p.Terrain, ref p.PatchCoord, p.TempBuffer, ref p.ModifiedOffset, ref p.ModifiedSize); + // Update terrain patch + TerrainTools.ModifyHeightMap(patch.Terrain, ref patch.PatchCoord, patch.TempBuffer, ref patch.ModifiedOffset, ref patch.ModifiedSize); + } + + // Auto NavMesh rebuild + var brushBounds = gizmo.CursorBrushBounds; + gizmo.CurrentEditUndoAction.AddDirtyBounds(ref brushBounds); + + Profiler.EndEvent(); + } + + /// + public override unsafe void ApplyBrushToPatch(ref ApplyParams p) + { + // noop; unused } } } diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 24c8f1c4c..ed6bd4915 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; @@ -194,7 +195,7 @@ namespace FlaxEditor.Viewport : base(Object.New(), editor.Undo, editor.Scene.Root) { _editor = editor; - DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType); + DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem); var inputOptions = editor.Options.Options.Input; // Prepare rendering task @@ -940,6 +941,11 @@ namespace FlaxEditor.Viewport return Level.IsAnySceneLoaded; } + private static bool ValidateDragScriptItem(ScriptItem script) + { + return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null; + } + /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 6e111f588..6c1509de8 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; @@ -81,7 +82,7 @@ namespace FlaxEditor.Viewport _window.SelectionChanged += OnSelectionChanged; Undo = window.Undo; ViewportCamera = new FPSCamera(); - DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType); + DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem); ShowDebugDraw = true; ShowEditorPrimitives = true; Gizmos = new GizmosCollection(this); @@ -702,6 +703,11 @@ namespace FlaxEditor.Viewport return true; } + private static bool ValidateDragScriptItem(ScriptItem script) + { + return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null; + } + /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs index 8a1b4f183..bf4ec7979 100644 --- a/Source/Editor/Viewport/ViewportDraggingHelper.cs +++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs @@ -39,17 +39,19 @@ namespace FlaxEditor.Viewport private readonly EditorViewport _viewport; private readonly DragAssets _dragAssets; private readonly DragActorType _dragActorType; + private readonly DragScriptItems _dragScriptItem; private StaticModel _previewStaticModel; private int _previewModelEntryIndex; private BrushSurface _previewBrushSurface; - internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func validateAsset, Func validateDragActorType) + internal ViewportDragHandlers(IGizmoOwner owner, EditorViewport viewport, Func validateAsset, Func validateDragActorType, Func validateDragScriptItem) { _owner = owner; _viewport = viewport; Add(_dragAssets = new DragAssets(validateAsset)); Add(_dragActorType = new DragActorType(validateDragActorType)); + Add(_dragScriptItem = new DragScriptItems(validateDragScriptItem)); } internal void ClearDragEffects() @@ -102,7 +104,12 @@ namespace FlaxEditor.Viewport foreach (var actorType in _dragActorType.Objects) Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal); } - + else if (_dragScriptItem.HasValidDrag) + { + result = _dragScriptItem.Effect; + foreach (var scripItem in _dragScriptItem.Objects) + Spawn(scripItem, hit, ref location, ref hitLocation, ref hitNormal); + } OnDragDrop(new DragDropEventArgs { Hit = hit, HitLocation = hitLocation }); return result; @@ -193,6 +200,15 @@ namespace FlaxEditor.Viewport _viewport.Focus(); } + private void Spawn(ScriptItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) + { + var actorType = Editor.Instance.CodeEditing.Actors.Get(item); + if (actorType != ScriptType.Null) + { + Spawn(actorType, hit, ref location, ref hitLocation, ref hitNormal); + } + } + private void Spawn(ScriptType item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) { var actor = item.CreateInstance() as Actor; diff --git a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs index 12eac22b5..e0d8c9ded 100644 --- a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs @@ -396,6 +396,16 @@ namespace FlaxEditor.Windows.Assets /// public override void OnUpdate() { + // Extract animations playback state from the events tracing + var debugActor = _debugPicker.Value as AnimatedModel; + if (debugActor == null) + debugActor = _preview.PreviewActor; + if (debugActor != null) + { + debugActor.EnableTracing = true; + Surface.LastTraceEvents = debugActor.TraceEvents; + } + base.OnUpdate(); // Update graph execution flow debugging visualization @@ -416,6 +426,8 @@ namespace FlaxEditor.Windows.Assets /// public override void OnDestroy() { + if (IsDisposing) + return; Animations.DebugFlow -= OnDebugFlow; _properties = null; diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index f263d4734..9418e7b75 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Drag; @@ -64,6 +65,7 @@ namespace FlaxEditor.Windows.Assets private PrefabWindow _window; private DragAssets _dragAssets; private DragActorType _dragActorType; + private DragScriptItems _dragScriptItems; private DragHandlers _dragHandlers; public SceneTreePanel(PrefabWindow window) @@ -84,6 +86,11 @@ namespace FlaxEditor.Windows.Assets return true; } + private static bool ValidateDragScriptItem(ScriptItem script) + { + return Editor.Instance.CodeEditing.Actors.Get(script); + } + /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { @@ -106,6 +113,13 @@ namespace FlaxEditor.Windows.Assets } if (_dragActorType.OnDragEnter(data)) return _dragActorType.Effect; + if (_dragScriptItems == null) + { + _dragScriptItems = new DragScriptItems(ValidateDragScriptItem); + _dragHandlers.Add(_dragScriptItems); + } + if (_dragScriptItems.OnDragEnter(data)) + return _dragScriptItems.Effect; } return result; } @@ -162,7 +176,27 @@ namespace FlaxEditor.Windows.Assets } result = DragDropEffect.Move; } - + // Drag script item + else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag) + { + for (int i = 0; i < _dragScriptItems.Objects.Count; i++) + { + var item = _dragScriptItems.Objects[i]; + var actorType = Editor.Instance.CodeEditing.Actors.Get(item); + if (actorType != ScriptType.Null) + { + var actor = actorType.CreateInstance() as Actor; + if (actor == null) + { + Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName); + continue; + } + actor.Name = actorType.Name; + _window.Spawn(actor); + } + } + result = DragDropEffect.Move; + } _dragHandlers.OnDragDrop(null); } return result; @@ -173,6 +207,7 @@ namespace FlaxEditor.Windows.Assets _window = null; _dragAssets = null; _dragActorType = null; + _dragScriptItems = null; _dragHandlers?.Clear(); _dragHandlers = null; diff --git a/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs b/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs index 7d09620d5..a564144c6 100644 --- a/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs +++ b/Source/Editor/Windows/Assets/SpriteAtlasWindow.cs @@ -83,14 +83,14 @@ namespace FlaxEditor.Windows.Assets set => Sprite.Name = value; } - [EditorOrder(1), Limit(-4096, 4096)] + [EditorOrder(1)] public Float2 Location { get => Sprite.Location; set => Sprite.Location = value; } - [EditorOrder(3), Limit(0, 4096)] + [EditorOrder(3), Limit(0)] public Float2 Size { get => Sprite.Size; diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index d43174cf3..68aa03678 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -799,7 +799,10 @@ namespace FlaxEditor.Windows if (proxy == null) throw new ArgumentNullException(nameof(proxy)); + // Setup name string name = initialName ?? proxy.NewItemName; + if (!proxy.IsFileNameValid(name) || Utilities.Utils.HasInvalidPathChar(name)) + name = proxy.NewItemName; // If the proxy can not be created in the current folder, then navigate to the content folder if (!proxy.CanCreate(CurrentViewFolder)) diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs index eb04f07b8..01e356187 100644 --- a/Source/Editor/Windows/PluginsWindow.cs +++ b/Source/Editor/Windows/PluginsWindow.cs @@ -95,6 +95,7 @@ namespace FlaxEditor.Windows Bounds = new Rectangle(nameLabel.X, tmp1, nameLabel.Width, Height - tmp1 - margin), }; + var xOffset = nameLabel.Width; string versionString = string.Empty; if (desc.IsAlpha) versionString = "ALPHA "; @@ -109,7 +110,7 @@ namespace FlaxEditor.Windows AnchorPreset = AnchorPresets.TopRight, Text = versionString, Parent = this, - Bounds = new Rectangle(Width - 140 - margin, margin, 140, 14), + Bounds = new Rectangle(Width - 140 - margin - xOffset, margin, 140, 14), }; string url = null; @@ -129,7 +130,7 @@ namespace FlaxEditor.Windows AnchorPreset = AnchorPresets.TopRight, Text = desc.Author, Parent = this, - Bounds = new Rectangle(Width - authorWidth - margin, versionLabel.Bottom + margin, authorWidth, 14), + Bounds = new Rectangle(Width - authorWidth - margin - xOffset, versionLabel.Bottom + margin, authorWidth, 14), }; if (url != null) { @@ -671,11 +672,11 @@ namespace FlaxEditor.Windows Editor.Log($"Using plugin code type name: {pluginCodeName}"); var oldPluginPath = Path.Combine(extractPath, "ExamplePlugin-master"); - var newPluginPath = Path.Combine(extractPath, pluginName); + var newPluginPath = Path.Combine(extractPath, pluginCodeName); Directory.Move(oldPluginPath, newPluginPath); var oldFlaxProjFile = Path.Combine(newPluginPath, "ExamplePlugin.flaxproj"); - var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginName}.flaxproj"); + var newFlaxProjFile = Path.Combine(newPluginPath, $"{pluginCodeName}.flaxproj"); File.Move(oldFlaxProjFile, newFlaxProjFile); var readme = Path.Combine(newPluginPath, "README.md"); @@ -687,7 +688,7 @@ namespace FlaxEditor.Windows // Flax plugin project file var flaxPluginProjContents = JsonSerializer.Deserialize(await File.ReadAllTextAsync(newFlaxProjFile)); - flaxPluginProjContents.Name = pluginName; + flaxPluginProjContents.Name = pluginCodeName; if (!string.IsNullOrEmpty(pluginVersion)) flaxPluginProjContents.Version = new Version(pluginVersion); if (!string.IsNullOrEmpty(companyName)) @@ -751,7 +752,7 @@ namespace FlaxEditor.Windows } Editor.Log($"Plugin project {pluginName} has successfully been created."); - await AddReferenceToProject(pluginName, pluginName); + await AddReferenceToProject(pluginCodeName, pluginCodeName); MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } @@ -775,8 +776,12 @@ namespace FlaxEditor.Windows var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs"); if (File.Exists(pluginModuleScriptPath)) { - gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");"); - modifiedAny = true; + var text = await File.ReadAllTextAsync(pluginModuleScriptPath); + if (!text.Contains("GameEditorModule", StringComparison.OrdinalIgnoreCase)) + { + gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");"); + modifiedAny = true; + } } } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 63ba7b960..97bf85b35 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FlaxEditor.Gizmo; using FlaxEditor.Content; using FlaxEditor.GUI.Tree; @@ -31,6 +32,7 @@ namespace FlaxEditor.Windows private DragAssets _dragAssets; private DragActorType _dragActorType; + private DragScriptItems _dragScriptItems; private DragHandlers _dragHandlers; /// @@ -273,6 +275,11 @@ namespace FlaxEditor.Windows return true; } + private static bool ValidateDragScriptItem(ScriptItem script) + { + return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null; + } + /// public override void Draw() { @@ -380,6 +387,13 @@ namespace FlaxEditor.Windows } if (_dragActorType.OnDragEnter(data) && result == DragDropEffect.None) return _dragActorType.Effect; + if (_dragScriptItems == null) + { + _dragScriptItems = new DragScriptItems(ValidateDragScriptItem); + _dragHandlers.Add(_dragScriptItems); + } + if (_dragScriptItems.OnDragEnter(data) && result == DragDropEffect.None) + return _dragScriptItems.Effect; } return result; } @@ -445,6 +459,28 @@ namespace FlaxEditor.Windows } result = DragDropEffect.Move; } + // Drag script item + else if (_dragScriptItems != null && _dragScriptItems.HasValidDrag) + { + for (int i = 0; i < _dragScriptItems.Objects.Count; i++) + { + var item = _dragScriptItems.Objects[i]; + var actorType = Editor.Instance.CodeEditing.Actors.Get(item); + if (actorType != ScriptType.Null) + { + var actor = actorType.CreateInstance() as Actor; + if (actor == null) + { + Editor.LogWarning("Failed to spawn actor of type " + actorType.TypeName); + continue; + } + actor.Name = actorType.Name; + Level.SpawnActor(actor); + Editor.Scene.MarkSceneEdited(actor.Scene); + } + } + result = DragDropEffect.Move; + } _dragHandlers.OnDragDrop(null); } @@ -456,6 +492,7 @@ namespace FlaxEditor.Windows { _dragAssets = null; _dragActorType = null; + _dragScriptItems = null; _dragHandlers?.Clear(); _dragHandlers = null; _tree = null; diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp index 48fa7fed1..c05b82b70 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp @@ -99,10 +99,7 @@ void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket) void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket) { - bucket.StateMachine.LastUpdateFrame = 0; - bucket.StateMachine.CurrentState = nullptr; - bucket.StateMachine.ActiveTransition = nullptr; - bucket.StateMachine.TransitionPosition = 0.0f; + Platform::MemoryClear(&bucket.StateMachine, sizeof(bucket.StateMachine)); } void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket) diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 7c4671a51..f7b883bd7 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -39,6 +39,7 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32 void AnimGraphInstanceData::Clear() { ClearState(); + Slots.Clear(); Parameters.Resize(0); } @@ -55,7 +56,7 @@ void AnimGraphInstanceData::ClearState() RootMotion = Transform::Identity; State.Resize(0); NodesPose.Resize(0); - Slots.Clear(); + TraceEvents.Clear(); } void AnimGraphInstanceData::Invalidate() @@ -238,6 +239,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) } for (auto& e : data.ActiveEvents) e.Hit = false; + data.TraceEvents.Clear(); // Init empty nodes data context.EmptyNodes.RootMotion = Transform::Identity; diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index 30ac5a776..c160b671c 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -129,6 +129,8 @@ public: UseDefaultRule = 4, InterruptionRuleRechecking = 8, InterruptionInstant = 16, + InterruptionSourceState = 32, + InterruptionDestinationState = 64, }; public: @@ -200,6 +202,21 @@ struct FLAXENGINE_API AnimGraphSlot bool Reset = false; }; +/// +/// The animation graph state container for a single node playback trace (eg. animation sample info or state transition). Can be used by Anim Graph debugging or custom scripting. +/// +API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent); + + // Contextual asset used. For example, sampled animation. + API_FIELD() Asset* Asset = nullptr; + // Generic value contextual to playback type (eg. animation sample position). + API_FIELD() float Value = 0; + // Identifier of the node in the graph. + API_FIELD() uint32 NodeId = 0; +}; + /// /// The animation graph instance data storage. Required to update the animation graph. /// @@ -241,7 +258,10 @@ public: uint64 LastUpdateFrame; AnimGraphNode* CurrentState; AnimGraphStateTransition* ActiveTransition; + AnimGraphStateTransition* BaseTransition; + AnimGraphNode* BaseTransitionState; float TransitionPosition; + float BaseTransitionPosition; }; struct SlotBucket @@ -358,6 +378,12 @@ public: /// void InvokeAnimEvents(); +public: + // Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations. + bool EnableTracing = false; + // Trace events collected when using EnableTracing option. + Array TraceEvents; + private: struct OutgoingEvent { @@ -837,7 +863,7 @@ public: } /// - /// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children). + /// Resets all the state bucket used by the given graph including sub-graphs (total). Can be used to reset the animation state of the nested graph (including children). /// void ResetBuckets(AnimGraphContext& context, AnimGraphBase* graph); @@ -866,5 +892,7 @@ private: Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC); Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode); Variant SampleState(AnimGraphNode* state); + void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr); + AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr); void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData); }; diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index fc7cf6dbc..d3b61c951 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -219,6 +219,16 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* const float animPos = GetAnimSamplePos(length, anim, pos, speed); const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed); + // Add to trace + auto& context = Context.Get(); + if (context.Data->EnableTracing) + { + auto& trace = context.Data->TraceEvents.AddOne(); + trace.Asset = anim; + trace.Value = animPos; + trace.NodeId = node->ID; + } + // Evaluate nested animations bool hasNested = false; if (anim->NestedAnims.Count() != 0) @@ -460,10 +470,12 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const { ANIM_GRAPH_PROFILE_EVENT("Blend Pose"); + if (isnan(alpha) || isinf(alpha)) + alpha = 0; + alpha = Math::Saturate(alpha); alpha = AlphaBlend::Process(alpha, alphaMode); const auto nodes = node->GetNodes(this); - auto nodesA = static_cast(poseA.AsPointer); auto nodesB = static_cast(poseB.AsPointer); if (!ANIM_GRAPH_IS_VALID_PTR(poseA)) @@ -484,32 +496,40 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const Variant AnimGraphExecutor::SampleState(AnimGraphNode* state) { - // Prepare auto& data = state->Data.State; if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr) - { - // Invalid state graph return Value::Null; - } - ANIM_GRAPH_PROFILE_EVENT("Evaluate State"); - - // Evaluate state auto rootNode = data.Graph->GetRootNode(); - auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]); - - return result; + return eatBox((Node*)rootNode, &rootNode->Boxes[0]); } -void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData) +void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition) +{ + // Reset transiton + stateMachineBucket.ActiveTransition = transition; + stateMachineBucket.TransitionPosition = 0.0f; + + // End base transition + if (stateMachineBucket.BaseTransition) + { + ResetBuckets(context, stateMachineBucket.BaseTransitionState->Data.State.Graph); + stateMachineBucket.BaseTransition = nullptr; + stateMachineBucket.BaseTransitionState = nullptr; + stateMachineBucket.BaseTransitionPosition = 0.0f; + } +} + +AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState) { int32 transitionIndex = 0; + const AnimGraphNode::StateBaseData& stateData = state->Data.State; while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex) { const uint16 idx = stateData.Transitions[transitionIndex]; ASSERT(idx < stateMachineData.Graph->StateTransitions.Count()); auto& transition = stateMachineData.Graph->StateTransitions[idx]; - if (transition.Destination == stateMachineBucket.CurrentState) + if (transition.Destination == state || transition.Destination == ignoreState) { // Ignore transition to the current state transitionIndex++; @@ -517,7 +537,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const } // Evaluate source state transition data (position, length, etc.) - const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState); + const Value sourceStatePtr = SampleState(state); auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule? if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr)) { @@ -538,6 +558,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const if (transition.RuleGraph && !useDefaultRule) { // Execute transition rule + ANIM_GRAPH_PROFILE_EVENT("Rule"); auto rootNode = transition.RuleGraph->GetRootNode(); ASSERT(rootNode); if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0])) @@ -560,10 +581,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const canEnter = true; if (canEnter) { - // Start transition - stateMachineBucket.ActiveTransition = &transition; - stateMachineBucket.TransitionPosition = 0.0f; - break; + return &transition; } // Skip after Solo transition @@ -573,6 +591,18 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const transitionIndex++; } + + // No transition + return nullptr; +} + +void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData) +{ + AnimGraphStateTransition* transition = UpdateStateTransitions(context, stateMachineData, stateMachineBucket.CurrentState); + if (transition) + { + InitStateTransition(context, stateMachineBucket, transition); + } } void ComputeMultiBlendLength(float& length, AnimGraphNode* node) @@ -1101,6 +1131,17 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu { const float alpha = Math::Saturate((float)tryGetValue(node->GetBox(3), node->Values[0])); auto mask = node->Assets[0].As(); + auto maskAssetBox = node->GetBox(4); // 4 is the id of skeleton mask parameter node. + + // Check if have some mask asset connected with the mask node + if (maskAssetBox->HasConnection()) + { + const Value assetBoxValue = tryGetValue(maskAssetBox, Value::Null); + + // Use the mask connected with this node instead of default mask asset + if (assetBoxValue != Value::Null) + mask = (SkeletonMask*)assetBoxValue.AsAsset; + } // Only A or missing/invalid mask if (Math::NearEqual(alpha, 0.0f, ANIM_GRAPH_BLEND_THRESHOLD) || mask == nullptr || mask->WaitForLoaded()) @@ -1501,10 +1542,9 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Blend two animations { - const float alpha = Math::Saturate(bucket.TransitionPosition / blendDuration); + const float alpha = bucket.TransitionPosition / blendDuration; const auto valueA = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + bucket.PreviousBlendPoseIndex), Value::Null); const auto valueB = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + poseIndex), Value::Null); - value = Blend(node, valueA, valueB, alpha, mode); } @@ -1610,22 +1650,21 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Enter to the first state pointed by the Entry node (without transitions) bucket.CurrentState = data.Graph->GetRootNode(); - bucket.ActiveTransition = nullptr; - bucket.TransitionPosition = 0.0f; + InitStateTransition(context, bucket); - // Reset all state buckets pof the graphs and nodes included inside the state machine + // Reset all state buckets of the graphs and nodes included inside the state machine ResetBuckets(context, data.Graph); } #define END_TRANSITION() \ ResetBuckets(context, bucket.CurrentState->Data.State.Graph); \ bucket.CurrentState = bucket.ActiveTransition->Destination; \ - bucket.ActiveTransition = nullptr; \ - bucket.TransitionPosition = 0.0f + InitStateTransition(context, bucket) // Update the active transition if (bucket.ActiveTransition) { bucket.TransitionPosition += context.DeltaTime; + ASSERT(bucket.CurrentState); // Check for transition end if (bucket.TransitionPosition >= bucket.ActiveTransition->BlendDuration) @@ -1633,38 +1672,70 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu END_TRANSITION(); } // Check for transition interruption - else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking)) + else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) && + EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) && + bucket.ActiveTransition->RuleGraph) { - const bool useDefaultRule = EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule); - if (bucket.ActiveTransition->RuleGraph && !useDefaultRule) + // Execute transition rule + auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode(); + if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0])) { - // Execute transition rule - auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode(); - if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0])) + bool cancelTransition = false; + if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) { - bool cancelTransition = false; - if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) + cancelTransition = true; + } + else + { + // Blend back to the source state (remove currently applied delta and rewind transition) + bucket.TransitionPosition -= context.DeltaTime; + bucket.TransitionPosition -= context.DeltaTime; + if (bucket.TransitionPosition <= ZeroTolerance) { cancelTransition = true; } - else - { - // Blend back to the source state (remove currently applied delta and rewind transition) - bucket.TransitionPosition -= context.DeltaTime; - bucket.TransitionPosition -= context.DeltaTime; - if (bucket.TransitionPosition <= ZeroTolerance) - { - cancelTransition = true; - } - } - if (cancelTransition) - { - // Go back to the source state - ResetBuckets(context, bucket.CurrentState->Data.State.Graph); - bucket.ActiveTransition = nullptr; - bucket.TransitionPosition = 0.0f; - } } + if (cancelTransition) + { + // Go back to the source state + ResetBuckets(context, bucket.CurrentState->Data.State.Graph); + InitStateTransition(context, bucket); + } + } + } + if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionSourceState)) + { + // Try to interrupt with any other transition in the source state (except the current transition) + if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.CurrentState, bucket.ActiveTransition->Destination)) + { + // Change active transition to the interrupted one + if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) + { + // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption) + bucket.BaseTransition = bucket.ActiveTransition; + bucket.BaseTransitionState = bucket.CurrentState; + bucket.BaseTransitionPosition = bucket.TransitionPosition; + } + bucket.ActiveTransition = transition; + bucket.TransitionPosition = 0.0f; + } + } + if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionDestinationState)) + { + // Try to interrupt with any other transition in the destination state (except the transition back to the current state if exists) + if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.ActiveTransition->Destination, bucket.CurrentState)) + { + // Change active transition to the interrupted one + if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) + { + // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption) + bucket.BaseTransition = bucket.ActiveTransition; + bucket.BaseTransitionState = bucket.CurrentState; + bucket.BaseTransitionPosition = bucket.TransitionPosition; + } + bucket.CurrentState = bucket.ActiveTransition->Destination; + bucket.ActiveTransition = transition; + bucket.TransitionPosition = 0.0f; } } } @@ -1693,9 +1764,23 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu } } - // Sample the current state - const auto currentState = SampleState(bucket.CurrentState); - value = currentState; + if (bucket.BaseTransitionState) + { + // Sample the other state (eg. when blending from interrupted state to the another state from the old destination) + value = SampleState(bucket.BaseTransitionState); + if (bucket.BaseTransition) + { + // Evaluate the base pose from the time when transition was interrupted + const auto destinationState = SampleState(bucket.BaseTransition->Destination); + const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration; + value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode); + } + } + else + { + // Sample the current state + value = SampleState(bucket.CurrentState); + } // Handle active transition blending if (bucket.ActiveTransition) @@ -1704,14 +1789,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu const auto destinationState = SampleState(bucket.ActiveTransition->Destination); // Perform blending - const float alpha = Math::Saturate(bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration); - value = Blend(node, currentState, destinationState, alpha, bucket.ActiveTransition->BlendMode); + const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration; + value = Blend(node, value, destinationState, alpha, bucket.ActiveTransition->BlendMode); } - // Update bucket bucket.LastUpdateFrame = context.CurrentFrameIndex; #undef END_TRANSITION - break; } // Entry @@ -2132,7 +2215,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Blend out auto input = tryGetValue(node->GetBox(1), Value::Null); bucket.BlendOutPosition += deltaTime; - const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime); + const float alpha = bucket.BlendOutPosition / slot.BlendOutTime; value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic); } else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime) @@ -2140,7 +2223,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Blend in auto input = tryGetValue(node->GetBox(1), Value::Null); bucket.BlendInPosition += deltaTime; - const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime); + const float alpha = bucket.BlendInPosition / slot.BlendInTime; value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic); } break; diff --git a/Source/Engine/Audio/AudioSource.cpp b/Source/Engine/Audio/AudioSource.cpp index cb32e0967..89d1d0a5b 100644 --- a/Source/Engine/Audio/AudioSource.cpp +++ b/Source/Engine/Audio/AudioSource.cpp @@ -19,6 +19,7 @@ AudioSource::AudioSource(const SpawnParams& params) , _minDistance(1000.0f) , _loop(false) , _playOnStart(false) + , _startTime(0.0f) , _allowSpatialization(true) { Clip.Changed.Bind(this); @@ -71,6 +72,11 @@ void AudioSource::SetPlayOnStart(bool value) _playOnStart = value; } +void AudioSource::SetStartTime(float value) +{ + _startTime = value; +} + void AudioSource::SetMinDistance(float value) { value = Math::Max(0.0f, value); @@ -361,6 +367,7 @@ void AudioSource::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(DopplerFactor, _dopplerFactor); SERIALIZE_MEMBER(Loop, _loop); SERIALIZE_MEMBER(PlayOnStart, _playOnStart); + SERIALIZE_MEMBER(StartTime, _startTime); SERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization); } @@ -377,6 +384,7 @@ void AudioSource::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE_MEMBER(DopplerFactor, _dopplerFactor); DESERIALIZE_MEMBER(Loop, _loop); DESERIALIZE_MEMBER(PlayOnStart, _playOnStart); + DESERIALIZE_MEMBER(StartTime, _startTime); DESERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization); DESERIALIZE(Clip); } @@ -540,5 +548,7 @@ void AudioSource::BeginPlay(SceneBeginData* data) return; #endif Play(); + if (GetStartTime() > 0) + SetTime(GetStartTime()); } } diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h index 70cdb4180..5c0c23c03 100644 --- a/Source/Engine/Audio/AudioSource.h +++ b/Source/Engine/Audio/AudioSource.h @@ -52,6 +52,7 @@ private: float _dopplerFactor = 1.0f; bool _loop; bool _playOnStart; + float _startTime; bool _allowSpatialization; bool _clipChanged = false; @@ -148,11 +149,25 @@ public: return _playOnStart; } + /// + /// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled. + /// + API_PROPERTY(Attributes = "EditorOrder(51), DefaultValue(0.0f), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Audio Source\", \"Start Time\"), VisibleIf(nameof(PlayOnStart))") + FORCE_INLINE float GetStartTime() const + { + return _startTime; + } + /// /// Determines whether the audio clip should auto play on game start. /// API_PROPERTY() void SetPlayOnStart(bool value); + /// + /// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled. + /// + API_PROPERTY() void SetStartTime(float value); + /// /// Gets the minimum distance at which audio attenuation starts. When the listener is closer to the source than this value, audio is heard at full volume. Once farther away the audio starts attenuating. /// diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index ace8b6591..3bd709259 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -116,7 +116,7 @@ namespace ALC { AudioBackend::Listener::TransformChanged(listener); - const Vector3 velocity = listener->GetVelocity(); + const Float3 velocity = listener->GetVelocity(); alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity)); alListenerf(AL_GAIN, Audio::GetVolume()); } @@ -319,8 +319,6 @@ void AudioBackendOAL::Listener_OnAdd(AudioListener* listener) ALC::RebuildContexts(false); #else AudioBackend::Listener::TransformChanged(listener); - const Vector3 velocity = listener->GetVelocity(); - alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity)); alListenerf(AL_GAIN, Audio::GetVolume()); #endif } @@ -336,7 +334,7 @@ void AudioBackendOAL::Listener_VelocityChanged(AudioListener* listener) { ALC_GET_LISTENER_CONTEXT(listener) - const Vector3 velocity = listener->GetVelocity(); + const Float3 velocity = listener->GetVelocity(); alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity)); } @@ -344,15 +342,15 @@ void AudioBackendOAL::Listener_TransformChanged(AudioListener* listener) { ALC_GET_LISTENER_CONTEXT(listener) - const Vector3 position = listener->GetPosition(); + const Float3 position = listener->GetPosition(); const Quaternion orientation = listener->GetOrientation(); - const Vector3 flipX(-1, 1, 1); - const Vector3 alOrientation[2] = + const Float3 flipX(-1, 1, 1); + const Float3 alOrientation[2] = { // Forward - orientation * Vector3::Forward * flipX, + orientation * Float3::Forward * flipX, // Up - orientation * Vector3::Up * flipX + orientation * Float3::Up * flipX }; alListenerfv(AL_ORIENTATION, (float*)alOrientation); diff --git a/Source/Engine/Core/Config/LayersAndTagsSettings.cs b/Source/Engine/Core/Config/LayersAndTagsSettings.cs index 008f70504..d5ebbe19b 100644 --- a/Source/Engine/Core/Config/LayersAndTagsSettings.cs +++ b/Source/Engine/Core/Config/LayersAndTagsSettings.cs @@ -18,7 +18,7 @@ namespace FlaxEditor.Content.Settings /// /// The layers names. /// - [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true)] + [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true, Display = CollectionAttribute.DisplayType.Inline)] public string[] Layers = new string[32]; /// diff --git a/Source/Engine/Core/Math/Matrix3x3.cs b/Source/Engine/Core/Math/Matrix3x3.cs index f4487bb1e..44af4542b 100644 --- a/Source/Engine/Core/Math/Matrix3x3.cs +++ b/Source/Engine/Core/Math/Matrix3x3.cs @@ -56,13 +56,7 @@ using System.Runtime.InteropServices; namespace FlaxEngine { - /// - /// Represents a 3x3 Matrix ( contains only Scale and Rotation ). - /// - [Serializable] - [StructLayout(LayoutKind.Sequential, Pack = 4)] - // ReSharper disable once InconsistentNaming - public struct Matrix3x3 : IEquatable, IFormattable + partial struct Matrix3x3 : IEquatable, IFormattable { /// /// The size of the type, in bytes. @@ -135,9 +129,7 @@ namespace FlaxEngine /// The value that will be assigned to all components. public Matrix3x3(float value) { - M11 = M12 = M13 = - M21 = M22 = M23 = - M31 = M32 = M33 = value; + M11 = M12 = M13 = M21 = M22 = M23 = M31 = M32 = M33 = value; } /// diff --git a/Source/Engine/Core/Math/Matrix3x3.h b/Source/Engine/Core/Math/Matrix3x3.h index 344f68455..0680e7735 100644 --- a/Source/Engine/Core/Math/Matrix3x3.h +++ b/Source/Engine/Core/Math/Matrix3x3.h @@ -9,8 +9,9 @@ /// /// Represents a 3x3 mathematical matrix. /// -API_STRUCT(InBuild) struct FLAXENGINE_API Matrix3x3 +API_STRUCT() struct FLAXENGINE_API Matrix3x3 { + DECLARE_SCRIPTING_TYPE_MINIMAL(Matrix3x3); public: union { diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 952e648e6..9e8b69b2c 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -139,6 +139,24 @@ VariantType::VariantType(const StringAnsiView& typeName) return; } } + { + // Aliases + if (typeName == "FlaxEngine.Vector2") + { + new(this) VariantType(Vector2); + return; + } + if (typeName == "FlaxEngine.Vector3") + { + new(this) VariantType(Vector3); + return; + } + if (typeName == "FlaxEngine.Vector4") + { + new(this) VariantType(Vector4); + return; + } + } // Check case for array if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive)) @@ -3985,15 +4003,32 @@ void Variant::CopyStructure(void* src) { if (AsBlob.Data && src) { - const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(Type.TypeName)); + const StringAnsiView typeName(Type.TypeName); + const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName); if (typeHandle) { auto& type = typeHandle.GetType(); type.Struct.Copy(AsBlob.Data, src); } +#if USE_CSHARP + else if (const auto mclass = Scripting::FindClass(typeName)) + { + // Fallback to C#-only types + MCore::Thread::Attach(); + if (MANAGED_GC_HANDLE && mclass->IsValueType()) + { + MObject* instance = MCore::GCHandle::GetTarget(MANAGED_GC_HANDLE); + void* data = MCore::Object::Unbox(instance); + Platform::MemoryCopy(data, src, mclass->GetInstanceSize()); + } + } +#endif else { - Platform::MemoryCopy(AsBlob.Data, src, AsBlob.Length); + if (typeName.Length() != 0) + { + LOG(Warning, "Missing scripting type \'{0}\'", String(typeName)); + } } } } diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index ff40cc208..7e2329389 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1359,7 +1359,7 @@ bool Actor::IsPrefabRoot() const Actor* Actor::FindActor(const StringView& name) const { Actor* result = nullptr; - if (StringUtils::Compare(*_name, *name) == 0) + if (_name == name) { result = const_cast(this); } @@ -1393,7 +1393,7 @@ Actor* Actor::FindActor(const MClass* type) const Actor* Actor::FindActor(const MClass* type, const StringView& name) const { CHECK_RETURN(type, nullptr); - if (GetClass()->IsSubClassOf(type) && StringUtils::Compare(*_name, *name) == 0) + if (GetClass()->IsSubClassOf(type) && _name == name) return const_cast(this); for (auto child : Children) { diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 17f117173..319def475 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -225,6 +225,17 @@ void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose) _masterPose->AnimationUpdated.Bind(this); } +const Array& AnimatedModel::GetTraceEvents() const +{ +#if !BUILD_RELEASE + if (!GetEnableTracing()) + { + LOG(Warning, "Accessing AnimatedModel.TraceEvents with tracing disabled."); + } +#endif + return GraphInstance.TraceEvents; +} + #define CHECK_ANIM_GRAPH_PARAM_ACCESS() \ if (!AnimationGraph) \ { \ diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index 029e17b62..bd03e824e 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -259,6 +259,27 @@ public: /// The master pose actor to use. API_FUNCTION() void SetMasterPoseModel(AnimatedModel* masterPose); + /// + /// Enables extracting animation playback insights for debugging or custom scripting. + /// + API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetEnableTracing() const + { + return GraphInstance.EnableTracing; + } + + /// + /// Enables extracting animation playback insights for debugging or custom scripting. + /// + API_PROPERTY() void SetEnableTracing(bool value) + { + GraphInstance.EnableTracing = value; + } + + /// + /// Gets the trace events from the last animation update. Valid only when EnableTracing is active. + /// + API_PROPERTY(Attributes="HideInEditor, NoSerialize") const Array& GetTraceEvents() const; + public: /// /// Gets the anim graph instance parameters collection. diff --git a/Source/Engine/Localization/LocalizationSettings.h b/Source/Engine/Localization/LocalizationSettings.h index e2c546c8b..22a4f03e4 100644 --- a/Source/Engine/Localization/LocalizationSettings.h +++ b/Source/Engine/Localization/LocalizationSettings.h @@ -16,7 +16,7 @@ public: /// /// The list of the string localization tables used by the game. /// - API_FIELD() + API_FIELD(Attributes="Collection(Display = CollectionAttribute.DisplayType.Inline)") Array> LocalizedStringTables; /// diff --git a/Source/Engine/Physics/Actors/RigidBody.cpp b/Source/Engine/Physics/Actors/RigidBody.cpp index afbac6611..71151866a 100644 --- a/Source/Engine/Physics/Actors/RigidBody.cpp +++ b/Source/Engine/Physics/Actors/RigidBody.cpp @@ -300,6 +300,44 @@ void RigidBody::ClosestPoint(const Vector3& position, Vector3& result) const } } +void RigidBody::AddMovement(const Vector3& translation, const Quaternion& rotation) +{ + // filter rotation according to constraints + Quaternion allowedRotation; + if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockRotation)) + allowedRotation = Quaternion::Identity; + else + { + Float3 euler = rotation.GetEuler(); + if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationX)) + euler.X = 0; + if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationY)) + euler.Y = 0; + if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationZ)) + euler.Z = 0; + allowedRotation = Quaternion::Euler(euler); + } + + // filter translation according to the constraints + auto allowedTranslation = translation; + if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockPosition)) + allowedTranslation = Vector3::Zero; + else + { + if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionX)) + allowedTranslation.X = 0; + if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionY)) + allowedTranslation.Y = 0; + if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionZ)) + allowedTranslation.Z = 0; + } + Transform t; + t.Translation = _transform.Translation + allowedTranslation; + t.Orientation = _transform.Orientation * allowedRotation; + t.Scale = _transform.Scale; + SetTransform(t); +} + void RigidBody::OnCollisionEnter(const Collision& c) { CollisionEnter(c); diff --git a/Source/Engine/Physics/Actors/RigidBody.h b/Source/Engine/Physics/Actors/RigidBody.h index 2ef50ca7a..ca1428810 100644 --- a/Source/Engine/Physics/Actors/RigidBody.h +++ b/Source/Engine/Physics/Actors/RigidBody.h @@ -486,6 +486,7 @@ public: // [Actor] void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; + void AddMovement(const Vector3& translation, const Quaternion& rotation) override; // [IPhysicsActor] void* GetPhysicsActor() const override; diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index af6c6403b..99a5abc56 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -3312,7 +3312,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor) // Create vehicle drive auto drive4W = PxVehicleDrive4W::allocate(wheels.Count()); drive4W->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, Math::Max(wheels.Count() - 4, 0)); - drive4W->setToRestState(); drive4W->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST); drive4W->mDriveDynData.setUseAutoGears(gearbox.AutoGear); vehicle = drive4W; @@ -3355,7 +3354,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor) // Create vehicle drive auto driveNW = PxVehicleDriveNW::allocate(wheels.Count()); driveNW->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, wheels.Count()); - driveNW->setToRestState(); driveNW->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST); driveNW->mDriveDynData.setUseAutoGears(gearbox.AutoGear); vehicle = driveNW; @@ -3366,7 +3364,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor) // Create vehicle drive auto driveNo = PxVehicleNoDrive::allocate(wheels.Count()); driveNo->setup(PhysX, actorPhysX, *wheelsSimData); - driveNo->setToRestState(); vehicle = driveNo; break; } diff --git a/Source/Engine/Physics/Physics.cpp b/Source/Engine/Physics/Physics.cpp index 7087b2e70..5d9b218ec 100644 --- a/Source/Engine/Physics/Physics.cpp +++ b/Source/Engine/Physics/Physics.cpp @@ -203,8 +203,11 @@ void Physics::Simulate(float dt) void Physics::CollectResults() { - if (DefaultScene) - DefaultScene->CollectResults(); + for (PhysicsScene* scene : Scenes) + { + if (scene->GetAutoSimulation()) + scene->CollectResults(); + } } bool Physics::IsDuringSimulation() diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp index 181077125..fec322bc3 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp @@ -324,7 +324,19 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION; SetWindowLong(_handle, GWL_STYLE, lStyle); - SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + const Float2 clientSize = GetClientSize(); + const Float2 desktopSize = Platform::GetDesktopSize(); + // Move window and half size if it is larger than desktop size + if (clientSize.X >= desktopSize.X && clientSize.Y >= desktopSize.Y) + { + const Float2 halfSize = desktopSize * 0.5f; + const Float2 middlePos = halfSize * 0.5f; + SetWindowPos(_handle, nullptr, (int)middlePos.X, (int)middlePos.Y, (int)halfSize.X, (int)halfSize.Y, SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE); + } + else + { + SetWindowPos(_handle, nullptr, 0, 0, (int)clientSize.X, (int)clientSize.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + } if (maximized) { diff --git a/Source/Engine/Scripting/Attributes/CollectionAttribute.cs b/Source/Engine/Scripting/Attributes/CollectionAttribute.cs index ca8f45d39..cd2e962e6 100644 --- a/Source/Engine/Scripting/Attributes/CollectionAttribute.cs +++ b/Source/Engine/Scripting/Attributes/CollectionAttribute.cs @@ -10,6 +10,32 @@ namespace FlaxEngine [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)] public sealed class CollectionAttribute : Attribute { + /// + /// The display type for collections. + /// + public enum DisplayType + { + /// + /// Displays the default display type. + /// + Default, + + /// + /// Displays a header. + /// + Header, + + /// + /// Displays inline. + /// + Inline, + } + + /// + /// Gets or sets the display type. + /// + public DisplayType Display; + /// /// Gets or sets whether this collection is read-only. If true, applications using this collection should not allow to add or remove items. /// diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 31ea87948..90454b586 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -606,14 +606,15 @@ namespace FlaxEngine.GUI _popup.LostFocus += DestroyPopup; // Show dropdown popup - var locationRootSpace = Location + new Float2(0, Height); + var locationRootSpace = Location + new Float2(0, Height - Height * (1 - Scale.Y) / 2); var parent = Parent; while (parent != null && parent != root) { locationRootSpace = parent.PointToParent(ref locationRootSpace); parent = parent.Parent; } - _popup.Location = locationRootSpace; + _popup.Scale = Scale; + _popup.Location = locationRootSpace - new Float2(0, _popup.Height * (1 - _popup.Scale.Y) / 2); _popup.Parent = root; _popup.Focus(); _popup.StartMouseCapture(); diff --git a/Source/Engine/UI/GUI/Panels/Panel.cs b/Source/Engine/UI/GUI/Panels/Panel.cs index bae165fba..05cb9bd80 100644 --- a/Source/Engine/UI/GUI/Panels/Panel.cs +++ b/Source/Engine/UI/GUI/Panels/Panel.cs @@ -132,6 +132,22 @@ namespace FlaxEngine.GUI if (_alwaysShowScrollbars != value) { _alwaysShowScrollbars = value; + switch (_scrollBars) + { + case ScrollBars.None: + break; + case ScrollBars.Horizontal: + HScrollBar.Visible = value; + break; + case ScrollBars.Vertical: + VScrollBar.Visible = value; + break; + case ScrollBars.Both: + HScrollBar.Visible = value; + VScrollBar.Visible = value; + break; + default: break; + } PerformLayout(); } } diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index 64164ecec..ea2824e77 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -685,9 +685,9 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) case 36: { // Get value with structure data - Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection()); if (!node->GetBox(0)->HasConnection()) return; + Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection()); // Find type const StringView typeName(node->Values[0]); @@ -741,6 +741,12 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) return; } const ScriptingType& type = typeHandle.GetType(); + if (structureValue.Type.Type != VariantType::Structure) // If structureValue is eg. Float we can try to cast it to a required structure type + { + VariantType typeVariantType(typeNameAnsiView); + if (Variant::CanCast(structureValue, typeVariantType)) + structureValue = Variant::Cast(structureValue, typeVariantType); + } structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName()); if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 5e63f489d..4e632014a 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1970,7 +1970,7 @@ namespace Flax.Build.Bindings { if (i != 0) contents.Append(", "); - contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i); + contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)).Append(" arg" + i); } contents.Append(')').AppendLine(); contents.Append(" {").AppendLine(); @@ -2058,7 +2058,7 @@ namespace Flax.Build.Bindings { if (i != 0) contents.Append(", "); - contents.Append(eventInfo.Type.GenericArgs[i]); + contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)); } contents.Append(")> f;").AppendLine(); if (eventInfo.IsStatic) @@ -2084,7 +2084,7 @@ namespace Flax.Build.Bindings { if (i != 0) contents.Append(", "); - contents.Append(eventInfo.Type.GenericArgs[i]).Append(" arg" + i); + contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)).Append(" arg" + i); } contents.Append(')').AppendLine(); contents.Append(" {").AppendLine(); @@ -2111,7 +2111,7 @@ namespace Flax.Build.Bindings { if (i != 0) contents.Append(", "); - contents.Append(eventInfo.Type.GenericArgs[i]); + contents.Append(eventInfo.Type.GenericArgs[i].GetFullNameNative(buildData, classInfo)); } contents.Append(")> f;").AppendLine(); if (eventInfo.IsStatic) @@ -2423,28 +2423,29 @@ namespace Flax.Build.Bindings // Getter for structure field contents.AppendLine(" static void GetField(void* ptr, const String& name, Variant& value)"); contents.AppendLine(" {"); - for (var i = 0; i < structureInfo.Fields.Count; i++) + for (int i = 0, count = 0; i < structureInfo.Fields.Count; i++) { var fieldInfo = structureInfo.Fields[i]; if (fieldInfo.IsReadOnly || fieldInfo.IsStatic || fieldInfo.IsConstexpr || fieldInfo.Access == AccessLevel.Private) continue; - if (i == 0) + if (count == 0) contents.AppendLine($" if (name == TEXT(\"{fieldInfo.Name}\"))"); else contents.AppendLine($" else if (name == TEXT(\"{fieldInfo.Name}\"))"); contents.AppendLine($" value = {GenerateCppWrapperNativeToVariant(buildData, fieldInfo.Type, structureInfo, $"(({structureTypeNameNative}*)ptr)->{fieldInfo.Name}")};"); + count++; } contents.AppendLine(" }").AppendLine(); // Setter for structure field contents.AppendLine(" static void SetField(void* ptr, const String& name, const Variant& value)"); contents.AppendLine(" {"); - for (var i = 0; i < structureInfo.Fields.Count; i++) + for (int i = 0, count = 0; i < structureInfo.Fields.Count; i++) { var fieldInfo = structureInfo.Fields[i]; if (fieldInfo.IsReadOnly || fieldInfo.IsStatic || fieldInfo.IsConstexpr || fieldInfo.Access == AccessLevel.Private) continue; - if (i == 0) + if (count == 0) contents.AppendLine($" if (name == TEXT(\"{fieldInfo.Name}\"))"); else contents.AppendLine($" else if (name == TEXT(\"{fieldInfo.Name}\"))"); @@ -2460,6 +2461,7 @@ namespace Flax.Build.Bindings } else contents.AppendLine($" (({structureTypeNameNative}*)ptr)->{fieldInfo.Name} = {GenerateCppWrapperVariantToNative(buildData, fieldInfo.Type, structureInfo, "value")};"); + count++; } contents.AppendLine(" }"); diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs index 843bb60fa..0b71af086 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs @@ -47,6 +47,20 @@ namespace Flax.Build.Bindings } } + private class ParseException : Exception + { + public ParseException(ref ParsingContext context, string msg) + : base(GetParseErrorLocation(ref context, msg)) + { + } + } + + private static string GetParseErrorLocation(ref ParsingContext context, string msg) + { + // Make it a link clickable in Visual Studio build output + return $"{context.File.Name}({context.Tokenizer.CurrentLine}): {msg}"; + } + private static string[] ParseComment(ref ParsingContext context) { if (context.StringCache == null) @@ -180,7 +194,7 @@ namespace Flax.Build.Bindings case TokenType.RightParent: parameters.Add(tag); return parameters; - default: throw new Exception($"Expected right parent or next argument, but got {token.Type}."); + default: throw new ParseException(ref context, $"Expected right parent or next argument, but got {token.Type}."); } } } @@ -302,7 +316,7 @@ namespace Flax.Build.Bindings if (context.PreprocessorDefines.TryGetValue(length, out var define)) length = define; if (!int.TryParse(length, out type.ArraySize)) - throw new Exception($"Failed to parse the field {entry} array size '{length}'"); + throw new ParseException(ref context, $"Failed to parse the field {entry} array size '{length}'"); } private static List ParseFunctionParameters(ref ParsingContext context) @@ -354,7 +368,7 @@ namespace Flax.Build.Bindings if (valid) break; var location = "function parameter"; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {location} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{location}'")); break; } } @@ -483,8 +497,7 @@ namespace Flax.Build.Bindings desc.Inheritance = new List(); desc.Inheritance.Add(inheritType); token = context.Tokenizer.NextToken(); - while (token.Type == TokenType.CommentSingleLine - || token.Type == TokenType.CommentMultiLine) + while (token.Type == TokenType.CommentSingleLine || token.Type == TokenType.CommentMultiLine) { token = context.Tokenizer.NextToken(); } @@ -563,7 +576,7 @@ namespace Flax.Build.Bindings // Read 'class' keyword var token = context.Tokenizer.NextToken(); if (token.Value != "class") - throw new Exception($"Invalid {ApiTokens.Class} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); + throw new ParseException(ref context, $"Invalid {ApiTokens.Class} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); // Read specifiers while (true) @@ -644,7 +657,7 @@ namespace Flax.Build.Bindings ParseTypeTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -672,7 +685,7 @@ namespace Flax.Build.Bindings // Read 'class' keyword var token = context.Tokenizer.NextToken(); if (token.Value != "class") - throw new Exception($"Invalid {ApiTokens.Interface} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); + throw new ParseException(ref context, $"Invalid {ApiTokens.Interface} usage (expected 'class' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); // Read specifiers while (true) @@ -735,7 +748,7 @@ namespace Flax.Build.Bindings ParseTypeTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -815,13 +828,13 @@ namespace Flax.Build.Bindings { case "const": if (desc.IsConst) - throw new Exception($"Invalid double 'const' specifier in function {desc.Name} at line {context.Tokenizer.CurrentLine}."); + throw new ParseException(ref context, $"Invalid double 'const' specifier in function {desc.Name}"); desc.IsConst = true; break; case "override": desc.IsVirtual = true; break; - default: throw new Exception($"Unknown identifier '{token.Value}' in function {desc.Name} at line {context.Tokenizer.CurrentLine}."); + default: throw new ParseException(ref context, $"Unknown identifier '{token.Value}' in function {desc.Name}"); } } else if (token.Type == TokenType.LeftCurlyBrace) @@ -875,7 +888,7 @@ namespace Flax.Build.Bindings ParseMemberTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -892,11 +905,11 @@ namespace Flax.Build.Bindings var classInfo = context.ScopeInfo as ClassInfo; if (classInfo == null) - throw new Exception($"Found property {propertyName} outside the class at line {context.Tokenizer.CurrentLine}."); + throw new ParseException(ref context, $"Found property {propertyName} outside the class"); var isGetter = !functionInfo.ReturnType.IsVoid; if (!isGetter && functionInfo.Parameters.Count == 0) - throw new Exception($"Property {propertyName} setter method in class {classInfo.Name} has to have value parameter to set (line {context.Tokenizer.CurrentLine})."); + throw new ParseException(ref context, $"Property {propertyName} setter method in class {classInfo.Name} has to have value parameter to set (line {context.Tokenizer.CurrentLine})."); var propertyType = isGetter ? functionInfo.ReturnType : functionInfo.Parameters[0].Type; var propertyInfo = classInfo.Properties.FirstOrDefault(x => x.Name == propertyName); @@ -917,7 +930,7 @@ namespace Flax.Build.Bindings else { if (propertyInfo.IsStatic != functionInfo.IsStatic) - throw new Exception($"Property {propertyName} in class {classInfo.Name} has to have both getter and setter methods static or non-static (line {context.Tokenizer.CurrentLine})."); + throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} has to have both getter and setter methods static or non-static (line {context.Tokenizer.CurrentLine})."); } if (functionInfo.Tags != null) { @@ -934,9 +947,9 @@ namespace Flax.Build.Bindings } if (isGetter && propertyInfo.Getter != null) - throw new Exception($"Property {propertyName} in class {classInfo.Name} cannot have multiple getter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter."); + throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} cannot have multiple getter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter."); if (!isGetter && propertyInfo.Setter != null) - throw new Exception($"Property {propertyName} in class {classInfo.Name} cannot have multiple setter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter."); + throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} cannot have multiple setter method (line {context.Tokenizer.CurrentLine}). Getter methods of properties must return value, while setters take this as a parameter."); if (isGetter) propertyInfo.Getter = functionInfo; @@ -963,7 +976,7 @@ namespace Flax.Build.Bindings return propertyInfo; if (getterType.Type == "Array" && setterType.Type == "Span" && getterType.GenericArgs?.Count == 1 && setterType.GenericArgs?.Count == 1 && getterType.GenericArgs[0].Equals(setterType.GenericArgs[0])) return propertyInfo; - throw new Exception($"Property {propertyName} in class {classInfo.Name} (line {context.Tokenizer.CurrentLine}) has mismatching getter return type ({getterType}) and setter parameter type ({setterType}). Both getter and setter methods must use the same value type used for property."); + throw new ParseException(ref context, $"Property {propertyName} in class {classInfo.Name} (line {context.Tokenizer.CurrentLine}) has mismatching getter return type ({getterType}) and setter parameter type ({setterType}). Both getter and setter methods must use the same value type used for property."); } if (propertyInfo.Comment != null) @@ -996,7 +1009,7 @@ namespace Flax.Build.Bindings // Read 'enum' or `enum class` keywords var token = context.Tokenizer.NextToken(); if (token.Value != "enum") - throw new Exception($"Invalid {ApiTokens.Enum} usage at line {context.Tokenizer.CurrentLine} (expected 'enum' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); + throw new ParseException(ref context, $"Invalid {ApiTokens.Enum} usage (expected 'enum' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); token = context.Tokenizer.NextToken(); if (token.Value != "class") context.Tokenizer.PreviousToken(); @@ -1079,7 +1092,7 @@ namespace Flax.Build.Bindings entry.Attributes = tag.Value; break; default: - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name + " enum entry"} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -1153,7 +1166,7 @@ namespace Flax.Build.Bindings ParseTypeTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -1176,7 +1189,7 @@ namespace Flax.Build.Bindings // Read 'struct' keyword var token = context.Tokenizer.NextToken(); if (token.Value != "struct") - throw new Exception($"Invalid {ApiTokens.Struct} usage (expected 'struct' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); + throw new ParseException(ref context, $"Invalid {ApiTokens.Struct} usage (expected 'struct' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); // Read name desc.Name = desc.NativeName = ParseName(ref context); @@ -1233,7 +1246,7 @@ namespace Flax.Build.Bindings ParseTypeTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -1366,7 +1379,7 @@ namespace Flax.Build.Bindings ParseMemberTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -1396,7 +1409,7 @@ namespace Flax.Build.Bindings if (desc.Type.Type == "Action") desc.Type = new TypeInfo { Type = "Delegate", GenericArgs = new List() }; else if (desc.Type.Type != "Delegate") - throw new Exception($"Invalid {ApiTokens.Event} type. Only Action and Delegate<> types are supported. '{desc.Type}' used on event at line {context.Tokenizer.CurrentLine}."); + throw new ParseException(ref context, $"Invalid {ApiTokens.Event} type. Only Action and Delegate<> types are supported. '{desc.Type}' used on event."); // Read name desc.Name = ParseName(ref context); @@ -1438,7 +1451,7 @@ namespace Flax.Build.Bindings ParseMemberTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } @@ -1474,7 +1487,7 @@ namespace Flax.Build.Bindings // Read 'typedef' keyword var token = context.Tokenizer.NextToken(); if (token.Value != "typedef") - throw new Exception($"Invalid {ApiTokens.Typedef} usage (expected 'typedef' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); + throw new ParseException(ref context, $"Invalid {ApiTokens.Typedef} usage (expected 'typedef' keyword but got '{token.Value} {context.Tokenizer.NextToken().Value}')."); // Read type definition desc.Type = ParseType(ref context); @@ -1513,7 +1526,7 @@ namespace Flax.Build.Bindings ParseTypeTag?.Invoke(ref valid, tag, desc); if (valid) break; - Log.Warning($"Unknown or not supported tag parameter {tag} used on {desc.Name} at line {context.Tokenizer.CurrentLine}"); + Log.Warning(GetParseErrorLocation(ref context, $"Unknown or not supported tag parameter '{tag}' used on '{desc.Name}'")); break; } } diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index 5cc1b2205..3438f8960 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -296,7 +296,7 @@ namespace Flax.Build.Projects.VisualStudio var folderIdMatches = new Regex("Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"(.*?)\", \"(.*?)\", \"{(.*?)}\"").Matches(contents); foreach (Match match in folderIdMatches) { - var folder = match.Groups[1].Value; + var folder = match.Groups[2].Value; var folderId = Guid.ParseExact(match.Groups[3].Value, "D"); folderIds[folder] = folderId; } @@ -385,8 +385,7 @@ namespace Flax.Build.Projects.VisualStudio { if (!folderIds.TryGetValue(folderPath, out project.FolderGuid)) { - if (!folderIds.TryGetValue(folderParents[i], out project.FolderGuid)) - project.FolderGuid = Guid.NewGuid(); + project.FolderGuid = Guid.NewGuid(); folderIds.Add(folderPath, project.FolderGuid); } folderNames.Add(folderPath); @@ -401,7 +400,7 @@ namespace Flax.Build.Projects.VisualStudio var lastSplit = folder.LastIndexOf('\\'); var name = lastSplit != -1 ? folder.Substring(lastSplit + 1) : folder; - vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, name, folderGuid)); + vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, folder, folderGuid)); vcSolutionFileContent.AppendLine("EndProject"); } }