Merge remote-tracking branch 'origin/master' into 1.7
# Conflicts: # Source/Engine/Level/Actors/AnimatedModel.cpp
This commit is contained in:
@@ -2,35 +2,35 @@
|
||||
using System.Collections.Generic;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace %namespace%
|
||||
namespace %namespace%;
|
||||
|
||||
/// <summary>
|
||||
/// %class% Script.
|
||||
/// </summary>
|
||||
public class %class% : Script
|
||||
{
|
||||
/// <summary>
|
||||
/// %class% Script.
|
||||
/// </summary>
|
||||
public class %class% : Script
|
||||
/// <inheritdoc/>
|
||||
public override void OnStart()
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override void OnStart()
|
||||
{
|
||||
// Here you can add code that needs to be called when script is created, just before the first game update
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable()
|
||||
{
|
||||
// Here you can add code that needs to be called when script is enabled (eg. register for events)
|
||||
}
|
||||
// Here you can add code that needs to be called when script is created, just before the first game update
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable()
|
||||
{
|
||||
// Here you can add code that needs to be called when script is enabled (eg. register for events)
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnDisable()
|
||||
{
|
||||
// Here you can add code that needs to be called when script is disabled (eg. unregister from events)
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override void OnDisable()
|
||||
{
|
||||
// Here you can add code that needs to be called when script is disabled (eg. unregister from events)
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnUpdate()
|
||||
{
|
||||
// Here you can add code that needs to be called every frame
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override void OnUpdate()
|
||||
{
|
||||
// Here you can add code that needs to be called every frame
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace FlaxEditor.Content.GUI
|
||||
_validDragOver = true;
|
||||
result = DragDropEffect.Copy;
|
||||
}
|
||||
else if (_dragActors.HasValidDrag)
|
||||
else if (_dragActors != null && _dragActors.HasValidDrag)
|
||||
{
|
||||
_validDragOver = true;
|
||||
result = DragDropEffect.Move;
|
||||
@@ -94,7 +94,7 @@ namespace FlaxEditor.Content.GUI
|
||||
result = DragDropEffect.Copy;
|
||||
}
|
||||
// Check if drop actor(s)
|
||||
else if (_dragActors.HasValidDrag)
|
||||
else if (_dragActors != null && _dragActors.HasValidDrag)
|
||||
{
|
||||
// Import actors
|
||||
var currentFolder = Editor.Instance.Windows.ContentWin.CurrentViewFolder;
|
||||
|
||||
@@ -139,7 +139,7 @@ namespace FlaxEditor.Content.Import
|
||||
var menu = new ContextMenu();
|
||||
menu.AddButton("Rename", OnRenameClicked);
|
||||
menu.AddButton("Don't import", OnDontImportClicked);
|
||||
menu.AddButton("Show in Explorer", OnShowInExplorerClicked);
|
||||
menu.AddButton(Utilities.Constants.ShowInExplorer, OnShowInExplorerClicked);
|
||||
menu.Tag = node;
|
||||
menu.Show(node, location);
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ namespace FlaxEditor.Content
|
||||
else
|
||||
Render2D.FillRectangle(rectangle, Color.Black);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Draws the item thumbnail.
|
||||
/// </summary>
|
||||
@@ -684,7 +684,7 @@ namespace FlaxEditor.Content
|
||||
var thumbnailSize = size.X;
|
||||
thumbnailRect = new Rectangle(0, 0, thumbnailSize, thumbnailSize);
|
||||
nameAlignment = TextAlignment.Center;
|
||||
|
||||
|
||||
if (this is ContentFolder)
|
||||
{
|
||||
// Small shadow
|
||||
@@ -692,7 +692,7 @@ namespace FlaxEditor.Content
|
||||
var color = Color.Black.AlphaMultiplied(0.2f);
|
||||
Render2D.FillRectangle(shadowRect, color);
|
||||
Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f));
|
||||
|
||||
|
||||
if (isSelected)
|
||||
Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
|
||||
else if (IsMouseOver)
|
||||
@@ -706,14 +706,14 @@ namespace FlaxEditor.Content
|
||||
var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1);
|
||||
var color = Color.Black.AlphaMultiplied(0.2f);
|
||||
Render2D.FillRectangle(shadowRect, color);
|
||||
|
||||
|
||||
Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f));
|
||||
Render2D.FillRectangle(TextRectangle, style.LightBackground);
|
||||
|
||||
|
||||
var accentHeight = 2 * view.ViewScale;
|
||||
var barRect = new Rectangle(0, thumbnailRect.Height - accentHeight, clientRect.Width, accentHeight);
|
||||
Render2D.FillRectangle(barRect, Color.DimGray);
|
||||
|
||||
|
||||
DrawThumbnail(ref thumbnailRect, false);
|
||||
if (isSelected)
|
||||
{
|
||||
@@ -733,18 +733,18 @@ namespace FlaxEditor.Content
|
||||
var thumbnailSize = size.Y - 2 * DefaultMarginSize;
|
||||
thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize);
|
||||
nameAlignment = TextAlignment.Near;
|
||||
|
||||
|
||||
if (isSelected)
|
||||
Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground);
|
||||
else if (IsMouseOver)
|
||||
Render2D.FillRectangle(clientRect, style.BackgroundHighlighted);
|
||||
|
||||
|
||||
DrawThumbnail(ref thumbnailRect);
|
||||
break;
|
||||
}
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
|
||||
// Draw short name
|
||||
Render2D.PushClip(ref textRect);
|
||||
Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 1f, 0.95f);
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace FlaxEditor.Content
|
||||
//_watcher.Changed += OnEvent;
|
||||
_watcher.Created += OnEvent;
|
||||
_watcher.Deleted += OnEvent;
|
||||
//_watcher.Renamed += OnEvent;
|
||||
_watcher.Renamed += OnEvent;
|
||||
}
|
||||
|
||||
private void OnEvent(object sender, FileSystemEventArgs e)
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
#include "Editor/ProjectInfo.h"
|
||||
#include "Editor/Cooker/GameCooker.h"
|
||||
#include "Editor/Utilities/EditorUtilities.h"
|
||||
#include <ThirdParty/pugixml/pugixml.hpp>
|
||||
|
||||
#include "pugixml/pugixml_extra.hpp"
|
||||
|
||||
using namespace pugi;
|
||||
|
||||
IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform);
|
||||
@@ -170,17 +172,17 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
|
||||
const String plistPath = data.DataOutputPath / TEXT("Info.plist");
|
||||
{
|
||||
xml_document doc;
|
||||
xml_node plist = doc.child_or_append(PUGIXML_TEXT("plist"));
|
||||
xml_node_extra plist = xml_node_extra(doc).child_or_append(PUGIXML_TEXT("plist"));
|
||||
plist.append_attribute(PUGIXML_TEXT("version")).set_value(PUGIXML_TEXT("1.0"));
|
||||
xml_node dict = plist.child_or_append(PUGIXML_TEXT("dict"));
|
||||
xml_node_extra dict = plist.child_or_append(PUGIXML_TEXT("dict"));
|
||||
|
||||
#define ADD_ENTRY(key, value) \
|
||||
dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \
|
||||
dict.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT(value))
|
||||
dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \
|
||||
dict.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT(value))
|
||||
#define ADD_ENTRY_STR(key, value) \
|
||||
dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \
|
||||
dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \
|
||||
{ std::u16string valueStr(value.GetText()); \
|
||||
dict.append_child(PUGIXML_TEXT("string")).set_child_value(pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); }
|
||||
dict.append_child_with_value(PUGIXML_TEXT("string"), pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); }
|
||||
|
||||
ADD_ENTRY("CFBundleDevelopmentRegion", "English");
|
||||
ADD_ENTRY("CFBundlePackageType", "APPL");
|
||||
@@ -194,22 +196,22 @@ bool MacPlatformTools::OnPostProcess(CookingData& data)
|
||||
ADD_ENTRY_STR("CFBundleVersion", projectVersion);
|
||||
ADD_ENTRY_STR("NSHumanReadableCopyright", gameSettings->CopyrightNotice);
|
||||
|
||||
dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("CFBundleSupportedPlatforms"));
|
||||
xml_node CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array"));
|
||||
CFBundleSupportedPlatforms.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("MacOSX"));
|
||||
dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("CFBundleSupportedPlatforms"));
|
||||
xml_node_extra CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array"));
|
||||
CFBundleSupportedPlatforms.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("MacOSX"));
|
||||
|
||||
dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture"));
|
||||
xml_node LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict"));
|
||||
dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture"));
|
||||
xml_node_extra LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict"));
|
||||
switch (_arch)
|
||||
{
|
||||
case ArchitectureType::x64:
|
||||
LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("x86_64"));
|
||||
LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("x86_64"));
|
||||
break;
|
||||
case ArchitectureType::ARM64:
|
||||
LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("arm64"));
|
||||
LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("arm64"));
|
||||
break;
|
||||
}
|
||||
LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("10.15"));
|
||||
LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("10.15"));
|
||||
|
||||
#undef ADD_ENTRY
|
||||
#undef ADD_ENTRY_STR
|
||||
|
||||
26
Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
Normal file
26
Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated;
|
||||
|
||||
/// <summary>
|
||||
/// The missing script editor.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(MissingScript)), DefaultEditor]
|
||||
public class MissingScriptEditor : GenericEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
if (layout.ContainerControl is not DropPanel dropPanel)
|
||||
{
|
||||
base.Initialize(layout);
|
||||
return;
|
||||
}
|
||||
|
||||
dropPanel.HeaderTextColor = Color.OrangeRed;
|
||||
|
||||
base.Initialize(layout);
|
||||
}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
|
||||
if (!CanEditTangent())
|
||||
return;
|
||||
|
||||
|
||||
var index = _lastPointSelected.Index;
|
||||
var currentTangentInPosition = _selectedSpline.GetSplineLocalTangent(index, true).Translation;
|
||||
var currentTangentOutPosition = _selectedSpline.GetSplineLocalTangent(index, false).Translation;
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked;
|
||||
|
||||
// Add button with the link icon
|
||||
|
||||
|
||||
_linkButton = new Button
|
||||
{
|
||||
BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32),
|
||||
|
||||
@@ -160,7 +160,6 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
var option = _options[comboBox.SelectedIndex];
|
||||
if (option.Type != null)
|
||||
value = option.Creator(option.Type);
|
||||
|
||||
}
|
||||
SetValue(value);
|
||||
RebuildLayoutOnRefresh();
|
||||
|
||||
@@ -634,7 +634,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
var textSize = FlaxEngine.GUI.Style.Current.FontMedium.MeasureText(buttonText);
|
||||
if (textSize.Y > button.Width)
|
||||
button.Width = textSize.Y + 2;
|
||||
|
||||
|
||||
button.SetAnchorPreset(AnchorPresets.MiddleRight, false, true);
|
||||
button.Clicked += ShowPicker;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace FlaxEditor.CustomEditors.GUI
|
||||
{
|
||||
// Clear flag
|
||||
_mouseOverSplitter = false;
|
||||
|
||||
|
||||
if (_cursorChanged)
|
||||
{
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
}
|
||||
|
||||
// Unlock and perform controls update
|
||||
Location = Float2.Zero;
|
||||
UnlockChildrenRecursive();
|
||||
PerformLayout();
|
||||
|
||||
@@ -162,7 +163,6 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
var dpiSize = Size * dpiScale;
|
||||
var locationWS = parent.PointToWindow(location);
|
||||
var locationSS = parentWin.PointToScreen(locationWS);
|
||||
Location = Float2.Zero;
|
||||
var monitorBounds = Platform.GetMonitorBounds(locationSS);
|
||||
var rightBottomLocationSS = locationSS + dpiSize;
|
||||
bool isUp = false, isLeft = false;
|
||||
|
||||
@@ -919,6 +919,11 @@ namespace FlaxEditor.Modules
|
||||
// Check if node already has that element (skip during init when we want to walk project dir very fast)
|
||||
if (_isDuringFastSetup || !parent.Folder.ContainsChild(path))
|
||||
{
|
||||
#if PLATFORM_MAC
|
||||
if (path.EndsWith(".DS_Store", StringComparison.Ordinal))
|
||||
continue;
|
||||
#endif
|
||||
|
||||
// Create file item
|
||||
ContentItem item;
|
||||
if (path.EndsWith(".cs"))
|
||||
@@ -960,6 +965,11 @@ namespace FlaxEditor.Modules
|
||||
// Check if node already has that element (skip during init when we want to walk project dir very fast)
|
||||
if (_isDuringFastSetup || !parent.Folder.ContainsChild(path))
|
||||
{
|
||||
#if PLATFORM_MAC
|
||||
if (path.EndsWith(".DS_Store", StringComparison.Ordinal))
|
||||
continue;
|
||||
#endif
|
||||
|
||||
// Create file item
|
||||
ContentItem item = null;
|
||||
if (FlaxEngine.Content.GetAssetInfo(path, out var assetInfo))
|
||||
@@ -1186,6 +1196,7 @@ namespace FlaxEditor.Modules
|
||||
{
|
||||
case WatcherChangeTypes.Created:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
case WatcherChangeTypes.Renamed:
|
||||
{
|
||||
lock (_dirtyNodes)
|
||||
{
|
||||
|
||||
@@ -553,6 +553,13 @@ namespace FlaxEditor.Modules
|
||||
cm.AddSeparator();
|
||||
_menuEditSelectAll = cm.AddButton("Select all", inputOptions.SelectAll, Editor.SceneEditing.SelectAllScenes);
|
||||
_menuEditFind = cm.AddButton("Find", inputOptions.Search, Editor.Windows.SceneWin.Search);
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Game Settings", () =>
|
||||
{
|
||||
var item = Editor.ContentDatabase.Find(GameSettings.GameSettingsAssetPath);
|
||||
if(item != null)
|
||||
Editor.ContentEditing.Open(item);
|
||||
});
|
||||
|
||||
// Scene
|
||||
MenuScene = MainMenu.AddButton("Scene");
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace FlaxEditor.Options
|
||||
[DefaultValue(60.0f), Limit(0, 666)]
|
||||
[EditorDisplay("General", "Editor FPS"), EditorOrder(110), Tooltip("Limit for the editor draw/update frames per second rate (FPS). Use higher values if you need more responsive interface or lower values to use less device power. Value 0 disables any limits.")]
|
||||
public float EditorFPS { get; set; } = 60.0f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets The FPS of the editor when the editor window is not focused. Usually set to lower then the editor FPS.
|
||||
/// </summary>
|
||||
@@ -203,7 +203,7 @@ namespace FlaxEditor.Options
|
||||
[DefaultValue(5), Limit(1)]
|
||||
[EditorDisplay("Auto Save", "Auto Save Frequency"), EditorOrder(801), Tooltip("The interval between auto saves (in minutes)")]
|
||||
public int AutoSaveFrequency { get; set; } = 5;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the time before the auto save that the popup shows (in seconds).
|
||||
/// </summary>
|
||||
|
||||
@@ -166,7 +166,19 @@ namespace FlaxEditor.Options
|
||||
/// Gets or sets the output log text font.
|
||||
/// </summary>
|
||||
[EditorDisplay("Output Log", "Text Font"), EditorOrder(320), Tooltip("The output log text font.")]
|
||||
public FontReference OutputLogTextFont { get; set; } = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
|
||||
public FontReference OutputLogTextFont
|
||||
{
|
||||
get => _outputLogFont;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
_outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
|
||||
else if (!value.Font)
|
||||
_outputLogFont.Font = FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont);
|
||||
else
|
||||
_outputLogFont = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output log text color.
|
||||
@@ -225,29 +237,82 @@ namespace FlaxEditor.Options
|
||||
public int NumberOfGameClientsToLaunch = 1;
|
||||
|
||||
private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.PrimaryFont);
|
||||
private FontReference _titleFont = new FontReference(DefaultFont, 18);
|
||||
private FontReference _largeFont = new FontReference(DefaultFont, 14);
|
||||
private FontReference _mediumFont = new FontReference(DefaultFont, 9);
|
||||
private FontReference _smallFont = new FontReference(DefaultFont, 9);
|
||||
private FontReference _outputLogFont = new FontReference(FlaxEngine.Content.LoadAsyncInternal<FontAsset>(EditorAssets.InconsolataRegularFont), 10);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title font for editor UI.
|
||||
/// </summary>
|
||||
[EditorDisplay("Fonts"), EditorOrder(600), Tooltip("The title font for editor UI.")]
|
||||
public FontReference TitleFont { get; set; } = new FontReference(DefaultFont, 18);
|
||||
public FontReference TitleFont
|
||||
{
|
||||
get => _titleFont;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
_titleFont = new FontReference(DefaultFont, 18);
|
||||
else if (!value.Font)
|
||||
_titleFont.Font = DefaultFont;
|
||||
else
|
||||
_titleFont = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the large font for editor UI.
|
||||
/// </summary>
|
||||
[EditorDisplay("Fonts"), EditorOrder(610), Tooltip("The large font for editor UI.")]
|
||||
public FontReference LargeFont { get; set; } = new FontReference(DefaultFont, 14);
|
||||
public FontReference LargeFont
|
||||
{
|
||||
get => _largeFont;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
_largeFont = new FontReference(DefaultFont, 14);
|
||||
else if (!value.Font)
|
||||
_largeFont.Font = DefaultFont;
|
||||
else
|
||||
_largeFont = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the medium font for editor UI.
|
||||
/// </summary>
|
||||
[EditorDisplay("Fonts"), EditorOrder(620), Tooltip("The medium font for editor UI.")]
|
||||
public FontReference MediumFont { get; set; } = new FontReference(DefaultFont, 9);
|
||||
public FontReference MediumFont
|
||||
{
|
||||
get => _mediumFont;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
_mediumFont = new FontReference(DefaultFont, 9);
|
||||
else if (!value.Font)
|
||||
_mediumFont.Font = DefaultFont;
|
||||
else
|
||||
_mediumFont = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the small font for editor UI.
|
||||
/// </summary>
|
||||
[EditorDisplay("Fonts"), EditorOrder(630), Tooltip("The small font for editor UI.")]
|
||||
public FontReference SmallFont { get; set; } = new FontReference(DefaultFont, 9);
|
||||
public FontReference SmallFont
|
||||
{
|
||||
get => _smallFont;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
_smallFont = new FontReference(DefaultFont, 9);
|
||||
else if (!value.Font)
|
||||
_smallFont.Font = DefaultFont;
|
||||
else
|
||||
_smallFont = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1393,5 +1393,32 @@ namespace FlaxEditor.Scripting
|
||||
return _custom.GetMembers(name, MemberTypes.Method, bindingAttr).FirstOrDefault();
|
||||
return ScriptMemberInfo.Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Basic check to see if a type could be casted to another type
|
||||
/// </summary>
|
||||
/// <param name="from">Source type</param>
|
||||
/// <param name="to">Target type</param>
|
||||
/// <returns>True if the type can be casted</returns>
|
||||
public static bool CanCast(ScriptType from, ScriptType to)
|
||||
{
|
||||
if (from == to)
|
||||
return true;
|
||||
if (from == Null || to == Null)
|
||||
return false;
|
||||
return (from.Type != typeof(void) && from.Type != typeof(FlaxEngine.Object)) &&
|
||||
(to.Type != typeof(void) && to.Type != typeof(FlaxEngine.Object)) &&
|
||||
from.IsAssignableFrom(to);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Basic check to see if this type could be casted to another type
|
||||
/// </summary>
|
||||
/// <param name="to">Target type</param>
|
||||
/// <returns>True if the type can be casted</returns>
|
||||
public bool CanCastTo(ScriptType to)
|
||||
{
|
||||
return CanCast(this, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,6 +744,16 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
return inputType.IsVoid;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InvokeMethodNode : SurfaceNode
|
||||
@@ -1151,6 +1161,54 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
if (nodeArch.Tag is not ScriptMemberInfo memberInfo)
|
||||
return false;
|
||||
|
||||
if (!memberInfo.IsStatic)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(memberInfo.DeclaringType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
|
||||
var parameters = memberInfo.GetParameters();
|
||||
bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid);
|
||||
if (outputType.IsVoid)
|
||||
return !isPure;
|
||||
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (param.IsOut)
|
||||
continue;
|
||||
if (VisjectSurface.FullCastCheck(param.Type, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
if (nodeArch.Tag is not ScriptMemberInfo memberInfo)
|
||||
return false;
|
||||
if (VisjectSurface.FullCastCheck(memberInfo.ValueType, inputType, hint))
|
||||
return true;
|
||||
|
||||
var parameters = memberInfo.GetParameters();
|
||||
bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid);
|
||||
if (inputType.IsVoid)
|
||||
return !isPure;
|
||||
|
||||
foreach (var param in memberInfo.GetParameters())
|
||||
{
|
||||
if (!param.IsOut)
|
||||
continue;
|
||||
if (VisjectSurface.FullCastCheck(param.Type, inputType, hint))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ReturnNode : SurfaceNode
|
||||
@@ -1777,6 +1835,16 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
return inputType.IsVoid;
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class FieldNodeBase : SurfaceNode
|
||||
@@ -1913,6 +1981,64 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Title = "Get " + SurfaceUtils.GetMethodDisplayName((string)Values[1]);
|
||||
UpdateSignature();
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
|
||||
if (scriptType == ScriptType.Null)
|
||||
return false;
|
||||
|
||||
var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (!SurfaceUtils.IsValidVisualScriptField(member))
|
||||
continue;
|
||||
|
||||
if (member)
|
||||
{
|
||||
if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var isStatic = (bool)nodeArch.DefaultValues[3];
|
||||
if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
|
||||
if (scriptType == ScriptType.Null)
|
||||
return false;
|
||||
|
||||
var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (!SurfaceUtils.IsValidVisualScriptField(member))
|
||||
continue;
|
||||
|
||||
if (member)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var typeName = (string)nodeArch.DefaultValues[2];
|
||||
if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), inputType, hint))
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SetFieldNode : FieldNodeBase
|
||||
@@ -1966,6 +2092,48 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Title = "Set " + SurfaceUtils.GetMethodDisplayName((string)Values[1]);
|
||||
UpdateSignature();
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
if (outputType.IsVoid)
|
||||
return true;
|
||||
|
||||
var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
|
||||
if (scriptType == ScriptType.Null)
|
||||
return false;
|
||||
|
||||
var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (!SurfaceUtils.IsValidVisualScriptField(member))
|
||||
continue;
|
||||
|
||||
if (member)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(member.ValueType, outputType, hint))
|
||||
return true;
|
||||
if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var typeName = (string)nodeArch.DefaultValues[2];
|
||||
if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), outputType, hint))
|
||||
return true;
|
||||
var isStatic = (bool)nodeArch.DefaultValues[3];
|
||||
if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
return inputType.IsVoid;
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class EventBaseNode : SurfaceNode, IFunctionsDependantNode
|
||||
@@ -2184,6 +2352,43 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
// Event based nodes always have a pulse input, so it's always compatible with void
|
||||
if (outputType.IsVoid)
|
||||
return true;
|
||||
|
||||
var eventName = (string)nodeArch.DefaultValues[1];
|
||||
var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
|
||||
var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
|
||||
if (member && SurfaceUtils.IsValidVisualScriptEvent(member))
|
||||
{
|
||||
if (!member.IsStatic)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(eventType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
// Event based nodes always have a pulse output, so it's always compatible with void
|
||||
if (inputType.IsVoid)
|
||||
return true;
|
||||
|
||||
var eventName = (string)nodeArch.DefaultValues[1];
|
||||
var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]);
|
||||
var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
|
||||
if (member && SurfaceUtils.IsValidVisualScriptEvent(member))
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BindEventNode : EventBaseNode
|
||||
@@ -2265,6 +2470,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Title = string.Empty,
|
||||
Description = "Overrides the base class method with custom implementation",
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste,
|
||||
IsInputCompatible = MethodOverrideNode.IsInputCompatible,
|
||||
IsOutputCompatible = MethodOverrideNode.IsOutputCompatible,
|
||||
Size = new Float2(240, 60),
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
@@ -2277,6 +2484,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 4,
|
||||
Create = (id, context, arch, groupArch) => new InvokeMethodNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = InvokeMethodNode.IsInputCompatible,
|
||||
IsOutputCompatible = InvokeMethodNode.IsOutputCompatible,
|
||||
Title = string.Empty,
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(240, 60),
|
||||
@@ -2317,6 +2526,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 6,
|
||||
Create = (id, context, arch, groupArch) => new VisualScriptFunctionNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = VisualScriptFunctionNode.IsInputCompatible,
|
||||
IsOutputCompatible = VisualScriptFunctionNode.IsOutputCompatible,
|
||||
Title = "New Function",
|
||||
Description = "Adds a new function to the script",
|
||||
Flags = NodeFlags.VisualScriptGraph,
|
||||
@@ -2330,6 +2541,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 7,
|
||||
Create = (id, context, arch, groupArch) => new GetFieldNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = GetFieldNode.IsInputCompatible,
|
||||
IsOutputCompatible = GetFieldNode.IsOutputCompatible,
|
||||
Title = string.Empty,
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(240, 60),
|
||||
@@ -2345,6 +2558,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 8,
|
||||
Create = (id, context, arch, groupArch) => new SetFieldNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = SetFieldNode.IsInputCompatible,
|
||||
IsOutputCompatible = SetFieldNode.IsOutputCompatible,
|
||||
Title = string.Empty,
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(240, 60),
|
||||
@@ -2361,6 +2576,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 9,
|
||||
Create = (id, context, arch, groupArch) => new BindEventNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = EventBaseNode.IsInputCompatible,
|
||||
IsOutputCompatible = EventBaseNode.IsOutputCompatible,
|
||||
Title = string.Empty,
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(260, 60),
|
||||
@@ -2383,6 +2600,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
{
|
||||
TypeID = 10,
|
||||
Create = (id, context, arch, groupArch) => new UnbindEventNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = EventBaseNode.IsInputCompatible,
|
||||
IsOutputCompatible = EventBaseNode.IsOutputCompatible,
|
||||
Title = string.Empty,
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(260, 60),
|
||||
|
||||
@@ -216,6 +216,35 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
: base(id, context, nodeArch, groupArch, false)
|
||||
{
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
var typeName = (string)nodeArch.DefaultValues[0];
|
||||
var type = TypeUtils.GetType(typeName);
|
||||
if (type)
|
||||
{
|
||||
var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray();
|
||||
var fieldsLength = fields.Length;
|
||||
for (var i = 0; i < fieldsLength; i++)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(fields[i].ValueType, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
var typeName = (string)nodeArch.DefaultValues[0];
|
||||
var type = TypeUtils.GetType(typeName);
|
||||
if (type)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(type, inputType, hint))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class UnpackStructureNode : StructureNode
|
||||
@@ -225,6 +254,35 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
: base(id, context, nodeArch, groupArch, true)
|
||||
{
|
||||
}
|
||||
|
||||
internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint)
|
||||
{
|
||||
var typeName = (string)nodeArch.DefaultValues[0];
|
||||
var type = TypeUtils.GetType(typeName);
|
||||
if (type)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(type, outputType, hint))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint)
|
||||
{
|
||||
var typeName = (string)nodeArch.DefaultValues[0];
|
||||
var type = TypeUtils.GetType(typeName);
|
||||
if (type)
|
||||
{
|
||||
var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray();
|
||||
var fieldsLength = fields.Length;
|
||||
for (var i = 0; i < fieldsLength; i++)
|
||||
{
|
||||
if (VisjectSurface.FullCastCheck(fields[i].ValueType, inputType, hint))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -351,6 +409,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
TypeID = 26,
|
||||
Title = "Pack Structure",
|
||||
Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = PackStructureNode.IsInputCompatible,
|
||||
IsOutputCompatible = PackStructureNode.IsOutputCompatible,
|
||||
Description = "Makes the structure data to from the components.",
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(180, 20),
|
||||
@@ -461,6 +521,8 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
TypeID = 36,
|
||||
Title = "Unpack Structure",
|
||||
Create = (id, context, arch, groupArch) => new UnpackStructureNode(id, context, arch, groupArch),
|
||||
IsInputCompatible = UnpackStructureNode.IsInputCompatible,
|
||||
IsOutputCompatible = UnpackStructureNode.IsOutputCompatible,
|
||||
Description = "Breaks the structure data to allow extracting components from it.",
|
||||
Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
|
||||
Size = new Float2(180, 20),
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.GUI.Input;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
using FlaxEngine.Utilities;
|
||||
@@ -41,6 +40,8 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
public delegate List<SurfaceParameter> ParameterGetterDelegate();
|
||||
|
||||
private readonly List<VisjectCMGroup> _groups = new List<VisjectCMGroup>(16);
|
||||
private CheckBox _contextSensitiveToggle;
|
||||
private bool _contextSensitiveSearchEnabled = true;
|
||||
private readonly TextBox _searchBox;
|
||||
private bool _waitingForInput;
|
||||
private VisjectCMGroup _surfaceParametersGroup;
|
||||
@@ -128,7 +129,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
_parameterSetNodeArchetype = info.ParameterSetNodeArchetype ?? Archetypes.Parameters.Nodes[3];
|
||||
|
||||
// Context menu dimensions
|
||||
Size = new Float2(320, 248);
|
||||
Size = new Float2(300, 400);
|
||||
|
||||
var headerPanel = new Panel(ScrollBars.None)
|
||||
{
|
||||
@@ -140,17 +141,41 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
};
|
||||
|
||||
// Title bar
|
||||
var titleFontReference = new FontReference(Style.Current.FontLarge.Asset, 10);
|
||||
var titleLabel = new Label
|
||||
{
|
||||
Width = Width - 8,
|
||||
Width = Width * 0.5f - 8f,
|
||||
Height = 20,
|
||||
X = 4,
|
||||
Parent = headerPanel,
|
||||
Text = "Select Node",
|
||||
HorizontalAlignment = TextAlignment.Center,
|
||||
Font = new FontReference(Style.Current.FontLarge.Asset, 10),
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
Font = titleFontReference,
|
||||
};
|
||||
|
||||
// Context sensitive toggle
|
||||
var contextSensitiveLabel = new Label
|
||||
{
|
||||
Width = Width * 0.5f - 28,
|
||||
Height = 20,
|
||||
X = Width * 0.5f,
|
||||
Parent = headerPanel,
|
||||
Text = "Context Sensitive",
|
||||
TooltipText = "Should the nodes be filtered to only show those that can be connected in the current context?",
|
||||
HorizontalAlignment = TextAlignment.Far,
|
||||
Font = titleFontReference,
|
||||
};
|
||||
|
||||
_contextSensitiveToggle = new CheckBox
|
||||
{
|
||||
Width = 20,
|
||||
Height = 20,
|
||||
X = Width - 24,
|
||||
Parent = headerPanel,
|
||||
Checked = _contextSensitiveSearchEnabled,
|
||||
};
|
||||
_contextSensitiveToggle.StateChanged += OnContextSensitiveToggleStateChanged;
|
||||
|
||||
// Search box
|
||||
_searchBox = new SearchBox(false, 2, 22)
|
||||
{
|
||||
@@ -291,6 +316,10 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
OnSearchFilterChanged();
|
||||
}
|
||||
}
|
||||
else if (_contextSensitiveSearchEnabled)
|
||||
{
|
||||
group.EvaluateVisibilityWithBox(_selectedBox);
|
||||
}
|
||||
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
@@ -324,6 +353,8 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
Parent = group
|
||||
};
|
||||
}
|
||||
if (_contextSensitiveSearchEnabled)
|
||||
group.EvaluateVisibilityWithBox(_selectedBox);
|
||||
group.SortChildren();
|
||||
if (ShowExpanded)
|
||||
group.Open(false);
|
||||
@@ -423,8 +454,26 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
return;
|
||||
|
||||
Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged");
|
||||
|
||||
if (string.IsNullOrEmpty(_searchBox.Text))
|
||||
UpdateFilters();
|
||||
_searchBox.Focus();
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
private void OnContextSensitiveToggleStateChanged(CheckBox checkBox)
|
||||
{
|
||||
// Skip events during setup or init stuff
|
||||
if (IsLayoutLocked)
|
||||
return;
|
||||
|
||||
Profiler.BeginEvent("VisjectCM.OnContextSensitiveToggleStateChanged");
|
||||
_contextSensitiveSearchEnabled = checkBox.Checked;
|
||||
UpdateFilters();
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
private void UpdateFilters()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null)
|
||||
{
|
||||
ResetView();
|
||||
Profiler.EndEvent();
|
||||
@@ -435,7 +484,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
LockChildrenRecursive();
|
||||
for (int i = 0; i < _groups.Count; i++)
|
||||
{
|
||||
_groups[i].UpdateFilter(_searchBox.Text);
|
||||
_groups[i].UpdateFilter(_searchBox.Text, _contextSensitiveSearchEnabled ? _selectedBox : null);
|
||||
_groups[i].UpdateItemSort(_selectedBox);
|
||||
}
|
||||
SortGroups();
|
||||
@@ -448,9 +497,6 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
PerformLayout();
|
||||
if (SelectedItem != null)
|
||||
_panel1.ScrollViewTo(SelectedItem);
|
||||
_searchBox.Focus();
|
||||
Profiler.EndEvent();
|
||||
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
@@ -508,7 +554,11 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
_searchBox.Clear();
|
||||
SelectedItem = null;
|
||||
for (int i = 0; i < _groups.Count; i++)
|
||||
{
|
||||
_groups[i].ResetView();
|
||||
if (_contextSensitiveSearchEnabled)
|
||||
_groups[i].EvaluateVisibilityWithBox(_selectedBox);
|
||||
}
|
||||
UnlockChildrenRecursive();
|
||||
|
||||
SortGroups();
|
||||
@@ -764,5 +814,12 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
{
|
||||
return GetPreviousSiblings(item).OfType<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
_contextSensitiveToggle.StateChanged -= OnContextSensitiveToggleStateChanged;
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
{
|
||||
if (_children[i] is VisjectCMItem item)
|
||||
{
|
||||
item.UpdateFilter(null);
|
||||
item.UpdateFilter(null, null);
|
||||
item.UpdateScore(null);
|
||||
}
|
||||
}
|
||||
@@ -84,23 +84,42 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
/// Updates the filter.
|
||||
/// </summary>
|
||||
/// <param name="filterText">The filter text.</param>
|
||||
public void UpdateFilter(string filterText)
|
||||
/// <param name="selectedBox">The optionally selected box to show hints for it.</param>
|
||||
public void UpdateFilter(string filterText, Box selectedBox)
|
||||
{
|
||||
Profiler.BeginEvent("VisjectCMGroup.UpdateFilter");
|
||||
|
||||
// Update items
|
||||
bool isAnyVisible = false;
|
||||
bool groupHeaderMatches = QueryFilterHelper.Match(filterText, HeaderText);
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
{
|
||||
if (_children[i] is VisjectCMItem item)
|
||||
{
|
||||
item.UpdateFilter(filterText);
|
||||
item.UpdateFilter(filterText, selectedBox, groupHeaderMatches);
|
||||
isAnyVisible |= item.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
// Update header title
|
||||
if (QueryFilterHelper.Match(filterText, HeaderText))
|
||||
// Update itself
|
||||
if (isAnyVisible)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filterText))
|
||||
Open(false);
|
||||
Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide group if none of the items matched the filter
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
Profiler.EndEvent();
|
||||
}
|
||||
|
||||
internal void EvaluateVisibilityWithBox(Box selectedBox)
|
||||
{
|
||||
if (selectedBox == null)
|
||||
{
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
{
|
||||
@@ -109,14 +128,25 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
item.Visible = true;
|
||||
}
|
||||
}
|
||||
isAnyVisible = true;
|
||||
Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Profiler.BeginEvent("VisjectCMGroup.EvaluateVisibilityWithBox");
|
||||
|
||||
bool isAnyVisible = false;
|
||||
for (int i = 0; i < _children.Count; i++)
|
||||
{
|
||||
if (_children[i] is VisjectCMItem item)
|
||||
{
|
||||
item.Visible = item.CanConnectTo(selectedBox);
|
||||
isAnyVisible |= item.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
// Update itself
|
||||
if (isAnyVisible)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filterText))
|
||||
Open(false);
|
||||
Visible = true;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Surface.Elements;
|
||||
using FlaxEditor.Utilities;
|
||||
using FlaxEngine;
|
||||
@@ -56,7 +57,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
/// <param name="groupArchetype">The group archetype.</param>
|
||||
/// <param name="archetype">The archetype.</param>
|
||||
public VisjectCMItem(VisjectCMGroup group, GroupArchetype groupArchetype, NodeArchetype archetype)
|
||||
: base(0, 0, 120, 12)
|
||||
: base(0, 0, 120, 14)
|
||||
{
|
||||
Group = group;
|
||||
_groupArchetype = groupArchetype;
|
||||
@@ -77,7 +78,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
if (selectedBox != null && CanConnectTo(selectedBox, NodeArchetype))
|
||||
if (selectedBox != null && CanConnectTo(selectedBox))
|
||||
SortScore += 1;
|
||||
if (Data != null)
|
||||
SortScore += 1;
|
||||
@@ -92,35 +93,93 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
textRect = new Rectangle(22, 0, Width - 24, Height);
|
||||
}
|
||||
|
||||
private bool CanConnectTo(Box startBox, NodeArchetype nodeArchetype)
|
||||
/// <summary>
|
||||
/// Checks if this context menu item can be connected to a given box, before a node is actually spawned.
|
||||
/// </summary>
|
||||
/// <param name="startBox">The connected box</param>
|
||||
/// <returns>True if the connected box is compatible with this item</returns>
|
||||
public bool CanConnectTo(Box startBox)
|
||||
{
|
||||
// Is compatible if box is null for reset reasons
|
||||
if (startBox == null)
|
||||
return false;
|
||||
if (!startBox.IsOutput)
|
||||
return false; // For now, I'm only handing the output box case
|
||||
return true;
|
||||
|
||||
if (nodeArchetype.Elements != null)
|
||||
if (_archetype == null)
|
||||
return false;
|
||||
|
||||
bool isCompatible = false;
|
||||
if (startBox.IsOutput && _archetype.IsInputCompatible != null)
|
||||
{
|
||||
for (int i = 0; i < nodeArchetype.Elements.Length; i++)
|
||||
{
|
||||
if (nodeArchetype.Elements[i].Type == NodeElementType.Input &&
|
||||
startBox.CanUseType(nodeArchetype.Elements[i].ConnectionsType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
isCompatible |= _archetype.IsInputCompatible.Invoke(_archetype, startBox.CurrentType, _archetype.ConnectionsHints);
|
||||
}
|
||||
return false;
|
||||
else if (!startBox.IsOutput && _archetype.IsOutputCompatible != null)
|
||||
{
|
||||
isCompatible |= _archetype.IsOutputCompatible.Invoke(_archetype, startBox.CurrentType, startBox.ParentNode.Archetype.ConnectionsHints);
|
||||
}
|
||||
else if (_archetype.Elements != null)
|
||||
{
|
||||
// Check compatibility based on the defined elements in the archetype. This handles all the default groups and items
|
||||
isCompatible = CheckElementsCompatibility(startBox);
|
||||
}
|
||||
|
||||
return isCompatible;
|
||||
}
|
||||
|
||||
private bool CheckElementsCompatibility(Box startBox)
|
||||
{
|
||||
bool isCompatible = false;
|
||||
foreach (NodeElementArchetype element in _archetype.Elements)
|
||||
{
|
||||
// Ignore all elements that aren't inputs or outputs (e.g. input fields)
|
||||
if (element.Type != NodeElementType.Output && element.Type != NodeElementType.Input)
|
||||
continue;
|
||||
|
||||
// Ignore elements with the same direction as the box
|
||||
if ((startBox.IsOutput && element.Type == NodeElementType.Output) || (!startBox.IsOutput && element.Type == NodeElementType.Input))
|
||||
continue;
|
||||
|
||||
ScriptType fromType;
|
||||
ScriptType toType;
|
||||
ConnectionsHint hint;
|
||||
if (startBox.IsOutput)
|
||||
{
|
||||
fromType = element.ConnectionsType;
|
||||
toType = startBox.CurrentType;
|
||||
hint = _archetype.ConnectionsHints;
|
||||
}
|
||||
else
|
||||
{
|
||||
fromType = startBox.CurrentType;
|
||||
toType = element.ConnectionsType;
|
||||
hint = startBox.ParentNode.Archetype.ConnectionsHints;
|
||||
}
|
||||
|
||||
isCompatible |= VisjectSurface.FullCastCheck(fromType, toType, hint);
|
||||
}
|
||||
|
||||
return isCompatible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the filter.
|
||||
/// </summary>
|
||||
/// <param name="filterText">The filter text.</param>
|
||||
public void UpdateFilter(string filterText)
|
||||
/// <param name="selectedBox">The optionally selected box to show hints for it.</param>
|
||||
/// <param name="groupHeaderMatches">True if item's group header got a filter match and item should stay visible.</param>
|
||||
public void UpdateFilter(string filterText, Box selectedBox, bool groupHeaderMatches = false)
|
||||
{
|
||||
if (selectedBox != null)
|
||||
{
|
||||
Visible = CanConnectTo(selectedBox);
|
||||
if (!Visible)
|
||||
{
|
||||
_highlights?.Clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_isStartsWithMatch = _isFullMatch = false;
|
||||
if (filterText == null)
|
||||
if (string.IsNullOrEmpty(filterText))
|
||||
{
|
||||
// Clear filter
|
||||
_highlights?.Clear();
|
||||
@@ -184,7 +243,7 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
|
||||
Data = data;
|
||||
}
|
||||
else
|
||||
else if (!groupHeaderMatches)
|
||||
{
|
||||
// Hide
|
||||
_highlights?.Clear();
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
_currentType = value;
|
||||
|
||||
// Check if will need to update box connections due to type change
|
||||
if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !CanCast(prev, _currentType))
|
||||
if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !prev.CanCastTo(_currentType))
|
||||
{
|
||||
// Remove all invalid connections and update those which still can be valid
|
||||
var connections = Connections.ToArray();
|
||||
@@ -236,58 +236,8 @@ namespace FlaxEditor.Surface.Elements
|
||||
}
|
||||
|
||||
// Check using connection hints
|
||||
var connectionsHints = ParentNode.Archetype.ConnectionsHints;
|
||||
if (Archetype.ConnectionsType == ScriptType.Null && connectionsHints != ConnectionsHint.None)
|
||||
{
|
||||
if ((connectionsHints & ConnectionsHint.Anything) == ConnectionsHint.Anything)
|
||||
return true;
|
||||
if ((connectionsHints & ConnectionsHint.Value) == ConnectionsHint.Value && type.Type != typeof(void))
|
||||
return true;
|
||||
if ((connectionsHints & ConnectionsHint.Enum) == ConnectionsHint.Enum && type.IsEnum)
|
||||
return true;
|
||||
if ((connectionsHints & ConnectionsHint.Array) == ConnectionsHint.Array && type.IsArray)
|
||||
return true;
|
||||
if ((connectionsHints & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && type.IsDictionary)
|
||||
return true;
|
||||
if ((connectionsHints & ConnectionsHint.Vector) == ConnectionsHint.Vector)
|
||||
{
|
||||
var t = type.Type;
|
||||
if (t == typeof(Vector2) ||
|
||||
t == typeof(Vector3) ||
|
||||
t == typeof(Vector4) ||
|
||||
t == typeof(Float2) ||
|
||||
t == typeof(Float3) ||
|
||||
t == typeof(Float4) ||
|
||||
t == typeof(Double2) ||
|
||||
t == typeof(Double3) ||
|
||||
t == typeof(Double4) ||
|
||||
t == typeof(Int2) ||
|
||||
t == typeof(Int3) ||
|
||||
t == typeof(Int4) ||
|
||||
t == typeof(Color))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((connectionsHints & ConnectionsHint.Scalar) == ConnectionsHint.Scalar)
|
||||
{
|
||||
var t = type.Type;
|
||||
if (t == typeof(bool) ||
|
||||
t == typeof(char) ||
|
||||
t == typeof(byte) ||
|
||||
t == typeof(short) ||
|
||||
t == typeof(ushort) ||
|
||||
t == typeof(int) ||
|
||||
t == typeof(uint) ||
|
||||
t == typeof(long) ||
|
||||
t == typeof(ulong) ||
|
||||
t == typeof(float) ||
|
||||
t == typeof(double))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (VisjectSurface.IsTypeCompatible(Archetype.ConnectionsType, type, ParentNode.Archetype.ConnectionsHints))
|
||||
return true;
|
||||
|
||||
// Check independent and if there is box with bigger potential because it may block current one from changing type
|
||||
var parentArch = ParentNode.Archetype;
|
||||
@@ -301,7 +251,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
var b = ParentNode.GetBox(boxes[i]);
|
||||
|
||||
// Check if its the same and tested type matches the default value type
|
||||
if (b == this && CanCast(parentArch.DefaultType, type))
|
||||
if (b == this && parentArch.DefaultType.CanCastTo(type))
|
||||
{
|
||||
// Can
|
||||
return true;
|
||||
@@ -690,17 +640,6 @@ namespace FlaxEditor.Surface.Elements
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanCast(ScriptType oB, ScriptType iB)
|
||||
{
|
||||
if (oB == iB)
|
||||
return true;
|
||||
if (oB == ScriptType.Null || iB == ScriptType.Null)
|
||||
return false;
|
||||
return (oB.Type != typeof(void) && oB.Type != typeof(FlaxEngine.Object)) &&
|
||||
(iB.Type != typeof(void) && iB.Type != typeof(FlaxEngine.Object)) &&
|
||||
oB.IsAssignableFrom(iB);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AreConnected(IConnectionInstigator other)
|
||||
{
|
||||
@@ -759,7 +698,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
{
|
||||
if (!iB.CanUseType(oB.CurrentType))
|
||||
{
|
||||
if (!CanCast(oB.CurrentType, iB.CurrentType))
|
||||
if (!oB.CurrentType.CanCastTo(iB.CurrentType))
|
||||
{
|
||||
// Cannot
|
||||
return false;
|
||||
@@ -770,7 +709,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
{
|
||||
if (!oB.CanUseType(iB.CurrentType))
|
||||
{
|
||||
if (!CanCast(oB.CurrentType, iB.CurrentType))
|
||||
if (!oB.CurrentType.CanCastTo(iB.CurrentType))
|
||||
{
|
||||
// Cannot
|
||||
return false;
|
||||
@@ -843,7 +782,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
bool useCaster = false;
|
||||
if (!iB.CanUseType(oB.CurrentType))
|
||||
{
|
||||
if (CanCast(oB.CurrentType, iB.CurrentType))
|
||||
if (oB.CurrentType.CanCastTo(iB.CurrentType))
|
||||
useCaster = true;
|
||||
else
|
||||
return;
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace FlaxEditor.Surface.Elements
|
||||
|
||||
// Calculate control points
|
||||
CalculateBezierControlPoints(start, end, out var control1, out var control2);
|
||||
|
||||
|
||||
// Draw line
|
||||
Render2D.DrawBezier(start, control1, control2, end, color, thickness);
|
||||
|
||||
@@ -61,16 +61,16 @@ namespace FlaxEditor.Surface.Elements
|
||||
const float maxControlLength = 150f;
|
||||
var dst = (end - start).Length;
|
||||
var yDst = Mathf.Abs(start.Y - end.Y);
|
||||
|
||||
|
||||
// Calculate control points
|
||||
var minControlDst = dst * 0.5f;
|
||||
var maxControlDst = Mathf.Max(Mathf.Min(maxControlLength, dst), minControlLength);
|
||||
var controlDst = Mathf.Lerp(minControlDst, maxControlDst, Mathf.Clamp(yDst / minControlLength, 0f, 1f));
|
||||
|
||||
|
||||
control1 = new Float2(start.X + controlDst, start.Y);
|
||||
control2 = new Float2(end.X - controlDst, end.Y);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a point intersects a connection
|
||||
/// </summary>
|
||||
@@ -93,11 +93,13 @@ namespace FlaxEditor.Surface.Elements
|
||||
public static bool IntersectsConnection(ref Float2 start, ref Float2 end, ref Float2 point, float distance)
|
||||
{
|
||||
// Pretty much a point in rectangle check
|
||||
if ((point.X - start.X) * (end.X - point.X) < 0) return false;
|
||||
if ((point.X - start.X) * (end.X - point.X) < 0)
|
||||
return false;
|
||||
|
||||
float offset = Mathf.Sign(end.Y - start.Y) * distance;
|
||||
if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0) return false;
|
||||
|
||||
if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0)
|
||||
return false;
|
||||
|
||||
float squaredDistance = distance;
|
||||
CalculateBezierControlPoints(start, end, out var control1, out var control2);
|
||||
|
||||
|
||||
@@ -89,6 +89,11 @@ namespace FlaxEditor.Surface
|
||||
/// <returns>The created node object.</returns>
|
||||
public delegate SurfaceNode CreateCustomNodeFunc(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given type is compatible with the given node archetype. Used for custom nodes
|
||||
/// </summary>
|
||||
public delegate bool IsCompatible(NodeArchetype nodeArch, ScriptType portType, ConnectionsHint hint);
|
||||
|
||||
/// <summary>
|
||||
/// Unique node type ID within a single group.
|
||||
/// </summary>
|
||||
@@ -99,6 +104,16 @@ namespace FlaxEditor.Surface
|
||||
/// </summary>
|
||||
public CreateCustomNodeFunc Create;
|
||||
|
||||
/// <summary>
|
||||
/// Function for asynchronously loaded nodes to check if input ports are compatible, for filtering.
|
||||
/// </summary>
|
||||
public IsCompatible IsInputCompatible;
|
||||
|
||||
/// <summary>
|
||||
/// Function for asynchronously loaded nodes to check if output ports are compatible, for filtering.
|
||||
/// </summary>
|
||||
public IsCompatible IsOutputCompatible;
|
||||
|
||||
/// <summary>
|
||||
/// Default initial size of the node.
|
||||
/// </summary>
|
||||
@@ -184,6 +199,8 @@ namespace FlaxEditor.Surface
|
||||
{
|
||||
TypeID = TypeID,
|
||||
Create = Create,
|
||||
IsInputCompatible = IsInputCompatible,
|
||||
IsOutputCompatible = IsOutputCompatible,
|
||||
Size = Size,
|
||||
Flags = Flags,
|
||||
Title = Title,
|
||||
|
||||
@@ -406,8 +406,8 @@ namespace FlaxEditor.Surface
|
||||
|
||||
internal static bool IsValidVisualScriptType(ScriptType scriptType)
|
||||
{
|
||||
if (!scriptType.IsPublic ||
|
||||
scriptType.HasAttribute(typeof(HideInEditorAttribute), true) ||
|
||||
if (!scriptType.IsPublic ||
|
||||
scriptType.HasAttribute(typeof(HideInEditorAttribute), true) ||
|
||||
scriptType.HasAttribute(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false))
|
||||
return false;
|
||||
if (scriptType.IsGenericType)
|
||||
|
||||
@@ -136,6 +136,88 @@ namespace FlaxEditor.Surface
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is compatible with another type and can be casted by using a connection hint
|
||||
/// </summary>
|
||||
/// <param name="from">Source type</param>
|
||||
/// <param name="to">Type to check compatibility with</param>
|
||||
/// <param name="hint">Hint to check if casting is possible</param>
|
||||
/// <returns>True if the source type is compatible with the target type</returns>
|
||||
public static bool IsTypeCompatible(ScriptType from, ScriptType to, ConnectionsHint hint)
|
||||
{
|
||||
if (from == ScriptType.Null && hint != ConnectionsHint.None)
|
||||
{
|
||||
if ((hint & ConnectionsHint.Anything) == ConnectionsHint.Anything)
|
||||
return true;
|
||||
if ((hint & ConnectionsHint.Value) == ConnectionsHint.Value && to.Type != typeof(void))
|
||||
return true;
|
||||
if ((hint & ConnectionsHint.Enum) == ConnectionsHint.Enum && to.IsEnum)
|
||||
return true;
|
||||
if ((hint & ConnectionsHint.Array) == ConnectionsHint.Array && to.IsArray)
|
||||
return true;
|
||||
if ((hint & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && to.IsDictionary)
|
||||
return true;
|
||||
if ((hint & ConnectionsHint.Vector) == ConnectionsHint.Vector)
|
||||
{
|
||||
var t = to.Type;
|
||||
if (t == typeof(Vector2) ||
|
||||
t == typeof(Vector3) ||
|
||||
t == typeof(Vector4) ||
|
||||
t == typeof(Float2) ||
|
||||
t == typeof(Float3) ||
|
||||
t == typeof(Float4) ||
|
||||
t == typeof(Double2) ||
|
||||
t == typeof(Double3) ||
|
||||
t == typeof(Double4) ||
|
||||
t == typeof(Int2) ||
|
||||
t == typeof(Int3) ||
|
||||
t == typeof(Int4) ||
|
||||
t == typeof(Color))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((hint & ConnectionsHint.Scalar) == ConnectionsHint.Scalar)
|
||||
{
|
||||
var t = to.Type;
|
||||
if (t == typeof(bool) ||
|
||||
t == typeof(char) ||
|
||||
t == typeof(byte) ||
|
||||
t == typeof(short) ||
|
||||
t == typeof(ushort) ||
|
||||
t == typeof(int) ||
|
||||
t == typeof(uint) ||
|
||||
t == typeof(long) ||
|
||||
t == typeof(ulong) ||
|
||||
t == typeof(float) ||
|
||||
t == typeof(double))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is compatible with another type and can be casted by using a connection hint
|
||||
/// </summary>
|
||||
/// <param name="from">Source type</param>
|
||||
/// <param name="to">Target type</param>
|
||||
/// <param name="hint">Connection hint</param>
|
||||
/// <returns>True if any method of casting or compatibility check succeeds</returns>
|
||||
public static bool FullCastCheck(ScriptType from, ScriptType to, ConnectionsHint hint)
|
||||
{
|
||||
// Yes, from and to are switched on purpose
|
||||
if (CanUseDirectCastStatic(to, from, false))
|
||||
return true;
|
||||
if (IsTypeCompatible(from, to, hint))
|
||||
return true;
|
||||
// Same here
|
||||
return to.CanCastTo(from);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if can use direct conversion from one type to another.
|
||||
/// </summary>
|
||||
|
||||
@@ -14,5 +14,11 @@ namespace FlaxEditor.Utilities
|
||||
public const string FacebookUrl = "https://facebook.com/FlaxEngine";
|
||||
public const string YoutubeUrl = "https://youtube.com/c/FlaxEngine";
|
||||
public const string TwitterUrl = "https://twitter.com/FlaxEngine";
|
||||
|
||||
#if PLATFORM_MAC
|
||||
public const string ShowInExplorer = "Show in Finder";
|
||||
#else
|
||||
public const string ShowInExplorer = "Show in explorer";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
|
||||
var tree = group.Tree();
|
||||
tree.TreeControl.RightClick += OnTreeNodeRightClick;
|
||||
tree.TreeControl.SelectedChanged += OnTreeSelectedChanged;
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
if (nodes[i].ParentIndex == -1)
|
||||
@@ -367,6 +368,12 @@ namespace FlaxEditor.Windows.Assets
|
||||
menu.Show(node, location);
|
||||
}
|
||||
|
||||
private void OnTreeSelectedChanged(List<TreeNode> before, List<TreeNode> after)
|
||||
{
|
||||
if (after.Count != 0)
|
||||
((SkeletonPropertiesProxy)Values[0]).Window._preview.ShowDebugDraw = true;
|
||||
}
|
||||
|
||||
private void OnTreeNodeCopyName(ContextMenuButton b)
|
||||
{
|
||||
Clipboard.Text = (string)b.Tag;
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace FlaxEditor.Windows
|
||||
|
||||
if (item is ContentFolder contentFolder && contentFolder.Node is ProjectTreeNode)
|
||||
{
|
||||
cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path));
|
||||
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path));
|
||||
}
|
||||
else if (isValidElement)
|
||||
{
|
||||
@@ -72,7 +72,7 @@ namespace FlaxEditor.Windows
|
||||
Open(e);
|
||||
});
|
||||
|
||||
cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path)));
|
||||
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path)));
|
||||
|
||||
if (item.HasDefaultThumbnail == false)
|
||||
{
|
||||
@@ -135,7 +135,7 @@ namespace FlaxEditor.Windows
|
||||
}
|
||||
else
|
||||
{
|
||||
cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path));
|
||||
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path));
|
||||
|
||||
b = cm.AddButton("Paste", _view.Paste);
|
||||
b.Enabled = _view.CanPaste();
|
||||
|
||||
@@ -988,7 +988,7 @@ namespace FlaxEditor.Windows
|
||||
}
|
||||
_view.ShowItems(items, _sortType, false, true);
|
||||
}
|
||||
else
|
||||
else if (target != null)
|
||||
{
|
||||
// Show folder contents
|
||||
var items = target.Folder.Children;
|
||||
|
||||
@@ -463,6 +463,18 @@ namespace FlaxEditor.Windows
|
||||
Cursor = CursorType.Default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseLeave()
|
||||
{
|
||||
base.OnMouseLeave();
|
||||
|
||||
// Remove focus from game window when mouse moves out and the cursor is hidden during game
|
||||
if ((IsFocused || ContainsFocus) && Parent != null && Editor.IsPlayMode && !Screen.CursorVisible)
|
||||
{
|
||||
Parent.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnShowContextMenu(ContextMenu menu)
|
||||
{
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace FlaxEditor.Windows
|
||||
_contextMenu.AddButton("Clear log", Clear);
|
||||
_contextMenu.AddButton("Copy selection", _output.Copy);
|
||||
_contextMenu.AddButton("Select All", _output.SelectAll);
|
||||
_contextMenu.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(Path.Combine(Globals.ProjectFolder, "Logs")));
|
||||
_contextMenu.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(Path.Combine(Globals.ProjectFolder, "Logs")));
|
||||
_contextMenu.AddButton("Scroll to bottom", () => { _vScroll.TargetValue = _vScroll.Maximum; }).Icon = Editor.Icons.ArrowDown12;
|
||||
|
||||
// Setup editor options
|
||||
|
||||
@@ -274,7 +274,7 @@ namespace FlaxEditor.Windows.Profiler
|
||||
ContextMenuButton b;
|
||||
b = cm.AddButton("Open", () => Editor.Instance.ContentEditing.Open(assetItem));
|
||||
cm.AddButton("Show in content window", () => Editor.Instance.Windows.ContentWin.Select(assetItem));
|
||||
cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path)));
|
||||
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path)));
|
||||
cm.AddButton("Select actors using this asset", () => Editor.Instance.SceneEditing.SelectActorsUsingAsset(assetItem.ID));
|
||||
cm.AddButton("Show asset references graph", () => Editor.Instance.Windows.Open(new AssetReferencesGraphWindow(Editor.Instance, assetItem)));
|
||||
cm.AddButton("Copy name", () => Clipboard.Text = assetItem.NamePath);
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace FlaxEditor.Windows.Search
|
||||
var cm = new FlaxEditor.GUI.ContextMenu.ContextMenu { Tag = assetItem };
|
||||
b = cm.AddButton("Open", () => Editor.Instance.ContentFinding.Open(Item));
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path)));
|
||||
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path)));
|
||||
cm.AddButton("Show in Content window", () => Editor.Instance.Windows.ContentWin.Select(assetItem, true));
|
||||
b.Enabled = proxy != null && proxy.CanReimport(assetItem);
|
||||
if (assetItem is BinaryAssetItem binaryAsset)
|
||||
|
||||
@@ -227,7 +227,7 @@ protected:
|
||||
|
||||
bool onLoad(LoadAssetTask* task);
|
||||
void onLoaded();
|
||||
void onLoaded_MainThread();
|
||||
virtual void onLoaded_MainThread();
|
||||
virtual void onUnload_MainThread();
|
||||
#if USE_EDITOR
|
||||
virtual void onRename(const StringView& newPath) = 0;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "FlaxEngine.Gen.h"
|
||||
#include "Cache/AssetsCache.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Config/Settings.h"
|
||||
#include "Engine/Serialization/JsonTools.h"
|
||||
#include "Engine/Serialization/JsonWriters.h"
|
||||
#include "Engine/Content/Factories/JsonAssetFactory.h"
|
||||
@@ -126,8 +127,7 @@ void FindIds(ISerializable::DeserializeStream& node, Array<Guid>& output)
|
||||
}
|
||||
else if (node.IsString())
|
||||
{
|
||||
const auto length = node.GetStringLength();
|
||||
if (length == 32)
|
||||
if (node.GetStringLength() == 32)
|
||||
{
|
||||
// Try parse as Guid in format `N` (32 hex chars)
|
||||
Guid id;
|
||||
@@ -362,10 +362,25 @@ void JsonAsset::unload(bool isReloading)
|
||||
#endif
|
||||
Scripting::ScriptsUnload.Unbind<JsonAsset, &JsonAsset::DeleteInstance>(this);
|
||||
DeleteInstance();
|
||||
_isAfterReload |= isReloading;
|
||||
|
||||
JsonAssetBase::unload(isReloading);
|
||||
}
|
||||
|
||||
void JsonAsset::onLoaded_MainThread()
|
||||
{
|
||||
JsonAssetBase::onLoaded_MainThread();
|
||||
|
||||
// Special case for Settings assets to flush them after edited and saved in Editor
|
||||
const StringAsANSI<> dataTypeNameAnsi(DataTypeName.Get(), DataTypeName.Length());
|
||||
const auto typeHandle = Scripting::FindScriptingType(StringAnsiView(dataTypeNameAnsi.Get(), DataTypeName.Length()));
|
||||
if (Instance && typeHandle && typeHandle.IsSubclassOf(SettingsBase::TypeInitializer) && _isAfterReload)
|
||||
{
|
||||
_isAfterReload = false;
|
||||
((SettingsBase*)Instance)->Apply();
|
||||
}
|
||||
}
|
||||
|
||||
bool JsonAsset::CreateInstance()
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
@@ -118,6 +118,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API JsonAsset : public JsonAssetBase
|
||||
DECLARE_ASSET_HEADER(JsonAsset);
|
||||
private:
|
||||
ScriptingType::Dtor _dtor;
|
||||
bool _isAfterReload = false;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -149,6 +150,7 @@ protected:
|
||||
// [JsonAssetBase]
|
||||
LoadResult loadAsset() override;
|
||||
void unload(bool isReloading) override;
|
||||
void onLoaded_MainThread() override;
|
||||
|
||||
private:
|
||||
bool CreateInstance();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "../Collections/Array.h"
|
||||
#include "../Collections/Dictionary.h"
|
||||
#include <functional>
|
||||
#include "../Delegate.h"
|
||||
|
||||
class ArrayExtensions;
|
||||
|
||||
@@ -23,7 +23,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the common key.
|
||||
/// </summary>
|
||||
/// <returns>The key.</returns>
|
||||
FORCE_INLINE const TKey& GetKey() const
|
||||
{
|
||||
return _key;
|
||||
@@ -32,7 +31,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the common key.
|
||||
/// </summary>
|
||||
/// <returns>The key.</returns>
|
||||
FORCE_INLINE TKey GetKey()
|
||||
{
|
||||
return _key;
|
||||
@@ -52,7 +50,7 @@ public:
|
||||
/// <param name="predicate">The prediction function. Should return true for the target element to find.</param>
|
||||
/// <returns>The index of the element or -1 if nothing found.</returns>
|
||||
template<typename T, typename AllocationType>
|
||||
static int32 IndexOf(const Array<T, AllocationType>& obj, const std::function<bool(const T&)>& predicate)
|
||||
static int32 IndexOf(const Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate)
|
||||
{
|
||||
for (int32 i = 0; i < obj.Count(); i++)
|
||||
{
|
||||
@@ -71,7 +69,7 @@ public:
|
||||
/// <param name="predicate">The prediction function.</param>
|
||||
/// <returns>True if any element in the collection matches the prediction, otherwise false.</returns>
|
||||
template<typename T, typename AllocationType>
|
||||
static bool Any(const Array<T, AllocationType>& obj, const std::function<bool(const T&)>& predicate)
|
||||
static bool Any(const Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate)
|
||||
{
|
||||
for (int32 i = 0; i < obj.Count(); i++)
|
||||
{
|
||||
@@ -90,7 +88,7 @@ public:
|
||||
/// <param name="predicate">The prediction function.</param>
|
||||
/// <returns>True if all elements in the collection matches the prediction, otherwise false.</returns>
|
||||
template<typename T, typename AllocationType>
|
||||
static int32 All(const Array<T, AllocationType>& obj, const std::function<bool(const T&)>& predicate)
|
||||
static int32 All(const Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate)
|
||||
{
|
||||
for (int32 i = 0; i < obj.Count(); i++)
|
||||
{
|
||||
@@ -109,7 +107,7 @@ public:
|
||||
/// <param name="keySelector">A function to extract the key for each element.</param>
|
||||
/// <param name="result">The result collection with groups.</param>
|
||||
template<typename TSource, typename TKey, typename AllocationType>
|
||||
static void GroupBy(const Array<TSource, AllocationType>& obj, const std::function<TKey(TSource const&)>& keySelector, Array<IGrouping<TKey, TSource>, AllocationType>& result)
|
||||
static void GroupBy(const Array<TSource, AllocationType>& obj, const Function<TKey(TSource const&)>& keySelector, Array<IGrouping<TKey, TSource>, AllocationType>& result)
|
||||
{
|
||||
Dictionary<TKey, IGrouping<TKey, TSource>> data(static_cast<int32>(obj.Count() * 3.0f));
|
||||
for (int32 i = 0; i < obj.Count(); i++)
|
||||
|
||||
@@ -105,6 +105,14 @@ int32 Engine::Main(const Char* cmdLine)
|
||||
Globals::StartupFolder = Globals::BinariesFolder = Platform::GetMainDirectory();
|
||||
#if USE_EDITOR
|
||||
Globals::StartupFolder /= TEXT("../../../..");
|
||||
#if PLATFORM_MAC
|
||||
if (Globals::BinariesFolder.EndsWith(TEXT(".app/Contents")))
|
||||
{
|
||||
// If running editor from application package on macOS
|
||||
Globals::StartupFolder = Globals::BinariesFolder;
|
||||
Globals::BinariesFolder /= TEXT("MacOS");
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
StringUtils::PathRemoveRelativeParts(Globals::StartupFolder);
|
||||
FileSystem::NormalizePath(Globals::BinariesFolder);
|
||||
@@ -122,7 +130,6 @@ int32 Engine::Main(const Char* cmdLine)
|
||||
}
|
||||
|
||||
EngineImpl::InitPaths();
|
||||
|
||||
EngineImpl::InitLog();
|
||||
|
||||
#if USE_EDITOR
|
||||
@@ -542,7 +549,8 @@ void EngineImpl::InitLog()
|
||||
LOG(Info, "Product: {0}, Company: {1}", Globals::ProductName, Globals::CompanyName);
|
||||
LOG(Info, "Current culture: {0}", Platform::GetUserLocaleName());
|
||||
LOG(Info, "Command line: {0}", CommandLine);
|
||||
LOG(Info, "Base directory: {0}", Globals::StartupFolder);
|
||||
LOG(Info, "Base folder: {0}", Globals::StartupFolder);
|
||||
LOG(Info, "Binaries folder: {0}", Globals::BinariesFolder);
|
||||
LOG(Info, "Temporary folder: {0}", Globals::TemporaryFolder);
|
||||
LOG(Info, "Project folder: {0}", Globals::ProjectFolder);
|
||||
#if USE_EDITOR
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace FlaxEngine
|
||||
{
|
||||
/// <summary>
|
||||
@@ -23,11 +25,17 @@ namespace FlaxEngine
|
||||
/// </summary>
|
||||
public float ValueRaw => Input.GetAxisRaw(Name);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when axis is changed. Called before scripts update.
|
||||
/// </summary>
|
||||
public event Action ValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputAxis"/> class.
|
||||
/// </summary>
|
||||
public InputAxis()
|
||||
{
|
||||
Input.AxisValueChanged += Handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,7 +44,31 @@ namespace FlaxEngine
|
||||
/// <param name="name">The axis name.</param>
|
||||
public InputAxis(string name)
|
||||
{
|
||||
Input.AxisValueChanged += Handler;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
private void Handler(string name)
|
||||
{
|
||||
if (string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
ValueChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="InputAxis"/> class.
|
||||
/// </summary>
|
||||
~InputAxis()
|
||||
{
|
||||
Input.AxisValueChanged -= Handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases this object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Input.AxisValueChanged -= Handler;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,36 @@ namespace FlaxEngine
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the event has been triggered during the current frame (e.g. user pressed a key). Use <see cref="Triggered"/> to catch events without active waiting.
|
||||
/// Returns true if the event has been triggered during the current frame (e.g. user pressed a key). Use <see cref="Pressed"/> to catch events without active waiting.
|
||||
/// </summary>
|
||||
public bool Active => Input.GetAction(Name);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the event state. Use Use <see cref="Pressed"/>, <see cref="Pressing"/>, <see cref="Released"/> to catch events without active waiting.
|
||||
/// </summary>
|
||||
public InputActionState State => Input.GetActionState(Name);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when event is triggered (e.g. user pressed a key). Called before scripts update.
|
||||
/// </summary>
|
||||
[System.Obsolete("Depreciated in 1.7, use Pressed Action.")]
|
||||
public event Action Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when event is pressed (e.g. user pressed a key). Called before scripts update.
|
||||
/// </summary>
|
||||
public event Action Pressed;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when event is being pressing (e.g. user pressing a key). Called before scripts update.
|
||||
/// </summary>
|
||||
public event Action Pressing;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when event is released (e.g. user releases a key). Called before scripts update.
|
||||
/// </summary>
|
||||
public event Action Released;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputEvent"/> class.
|
||||
/// </summary>
|
||||
@@ -51,10 +72,26 @@ namespace FlaxEngine
|
||||
Input.ActionTriggered -= Handler;
|
||||
}
|
||||
|
||||
private void Handler(string name)
|
||||
private void Handler(string name, InputActionState state)
|
||||
{
|
||||
if (string.Equals(name, Name, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(name, Name, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
switch (state)
|
||||
{
|
||||
case InputActionState.None: break;
|
||||
case InputActionState.Waiting: break;
|
||||
case InputActionState.Pressing:
|
||||
Pressing?.Invoke();
|
||||
break;
|
||||
case InputActionState.Press:
|
||||
Triggered?.Invoke();
|
||||
Pressed?.Invoke();
|
||||
break;
|
||||
case InputActionState.Release:
|
||||
Released?.Invoke();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -130,7 +130,7 @@ void FoliageType::Serialize(SerializeStream& stream, const void* otherObj)
|
||||
|
||||
SERIALIZE(Model);
|
||||
|
||||
const std::function<bool(const ModelInstanceEntry&)> IsValidMaterial = [](const ModelInstanceEntry& e) -> bool
|
||||
const Function<bool(const ModelInstanceEntry&)> IsValidMaterial = [](const ModelInstanceEntry& e) -> bool
|
||||
{
|
||||
return e.Material;
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ public:
|
||||
const auto parentModelIndex = node.ParentIndex;
|
||||
|
||||
// Find matching node in skeleton (or map to best parent)
|
||||
const std::function<bool(const T&)> f = [node](const T& x) -> bool
|
||||
const Function<bool(const T&)> f = [node](const T& x) -> bool
|
||||
{
|
||||
return x.Name == node.Name;
|
||||
};
|
||||
|
||||
@@ -459,9 +459,9 @@ void GPUContextVulkan::UpdateDescriptorSets(const SpirvShaderDescriptorInfo& des
|
||||
{
|
||||
case VK_DESCRIPTOR_TYPE_SAMPLER:
|
||||
{
|
||||
const VkSampler sampler = _samplerHandles[slot];
|
||||
ASSERT(sampler);
|
||||
needsWrite |= dsWriter.WriteSampler(descriptorIndex, sampler, index);
|
||||
const VkSampler handle = _samplerHandles[slot];
|
||||
ASSERT(handle);
|
||||
needsWrite |= dsWriter.WriteSampler(descriptorIndex, handle, index);
|
||||
break;
|
||||
}
|
||||
case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE:
|
||||
@@ -547,12 +547,18 @@ void GPUContextVulkan::UpdateDescriptorSets(const SpirvShaderDescriptorInfo& des
|
||||
}
|
||||
case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC:
|
||||
{
|
||||
auto cb = handles[slot];
|
||||
ASSERT(cb);
|
||||
VkBuffer buffer;
|
||||
VkDeviceSize offset, range;
|
||||
uint32 dynamicOffset;
|
||||
cb->DescriptorAsDynamicUniformBuffer(this, buffer, offset, range, dynamicOffset);
|
||||
auto handle = handles[slot];
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize offset = 0, range = 0;
|
||||
uint32 dynamicOffset = 0;
|
||||
if (handle)
|
||||
handle->DescriptorAsDynamicUniformBuffer(this, buffer, offset, range, dynamicOffset);
|
||||
else
|
||||
{
|
||||
const auto dummy = _device->HelperResources.GetDummyBuffer();
|
||||
buffer = dummy->GetHandle();
|
||||
range = dummy->GetSize();
|
||||
}
|
||||
needsWrite |= dsWriter.WriteDynamicUniformBuffer(descriptorIndex, buffer, offset, range, dynamicOffset, index);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ static const char* GValidationLayers[] =
|
||||
|
||||
static const char* GInstanceExtensions[] =
|
||||
{
|
||||
#if PLATFORM_APPLE_FAMILY && defined(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)
|
||||
VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
|
||||
VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME,
|
||||
#endif
|
||||
#if VK_EXT_validation_cache
|
||||
VK_EXT_VALIDATION_CACHE_EXTENSION_NAME,
|
||||
#endif
|
||||
@@ -46,6 +50,9 @@ static const char* GInstanceExtensions[] =
|
||||
|
||||
static const char* GDeviceExtensions[] =
|
||||
{
|
||||
#if PLATFORM_APPLE_FAMILY && defined(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME)
|
||||
VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME,
|
||||
#endif
|
||||
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
|
||||
#if VK_KHR_maintenance1
|
||||
VK_KHR_MAINTENANCE1_EXTENSION_NAME,
|
||||
@@ -571,7 +578,7 @@ void GPUDeviceVulkan::ParseOptionalDeviceExtensions(const Array<const char*>& de
|
||||
|
||||
const auto HasExtension = [&deviceExtensions](const char* name) -> bool
|
||||
{
|
||||
const std::function<bool(const char* const&)> CheckCallback = [&name](const char* const& extension) -> bool
|
||||
const Function<bool(const char* const&)> CheckCallback = [&name](const char* const& extension) -> bool
|
||||
{
|
||||
return StringUtils::Compare(extension, name) == 0;
|
||||
};
|
||||
|
||||
@@ -431,7 +431,7 @@ void DeferredDeletionQueueVulkan::EnqueueGenericResource(Type type, uint64 handl
|
||||
ScopeLock lock(_locker);
|
||||
|
||||
#if BUILD_DEBUG
|
||||
const std::function<bool(const Entry&)> ContainsHandle = [handle](const Entry& e)
|
||||
const Function<bool(const Entry&)> ContainsHandle = [handle](const Entry& e)
|
||||
{
|
||||
return e.Handle == handle;
|
||||
};
|
||||
@@ -868,7 +868,7 @@ GPUBufferVulkan* HelperResourcesVulkan::GetDummyBuffer()
|
||||
if (!_dummyBuffer)
|
||||
{
|
||||
_dummyBuffer = (GPUBufferVulkan*)_device->CreateBuffer(TEXT("DummyBuffer"));
|
||||
_dummyBuffer->Init(GPUBufferDescription::Buffer(sizeof(int32), GPUBufferFlags::ShaderResource | GPUBufferFlags::UnorderedAccess, PixelFormat::R32_SInt));
|
||||
_dummyBuffer->Init(GPUBufferDescription::Buffer(sizeof(int32) * 256, GPUBufferFlags::ShaderResource | GPUBufferFlags::UnorderedAccess, PixelFormat::R32_SInt));
|
||||
}
|
||||
|
||||
return _dummyBuffer;
|
||||
@@ -1078,13 +1078,16 @@ GPUDevice* GPUDeviceVulkan::Create()
|
||||
|
||||
VkInstanceCreateInfo instInfo;
|
||||
RenderToolsVulkan::ZeroStruct(instInfo, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);
|
||||
#if PLATFORM_APPLE_FAMILY
|
||||
instInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
|
||||
#endif
|
||||
instInfo.pApplicationInfo = &appInfo;
|
||||
|
||||
GetInstanceLayersAndExtensions(InstanceExtensions, InstanceLayers, SupportsDebugUtilsExt);
|
||||
|
||||
const auto hasExtension = [](const Array<const char*>& extensions, const char* name) -> bool
|
||||
{
|
||||
const std::function<bool(const char* const&)> callback = [&name](const char* const& extension) -> bool
|
||||
const Function<bool(const char* const&)> callback = [&name](const char* const& extension) -> bool
|
||||
{
|
||||
return extension && StringUtils::Compare(extension, name) == 0;
|
||||
};
|
||||
|
||||
@@ -51,7 +51,8 @@ public sealed class VulkanSdk : Sdk
|
||||
var subDirs = Directory.GetDirectories(path);
|
||||
if (subDirs.Length != 0)
|
||||
{
|
||||
path = Path.Combine(subDirs[0], "macOS");
|
||||
Flax.Build.Utilities.SortVersionDirectories(subDirs);
|
||||
path = Path.Combine(subDirs.Last(), "macOS");
|
||||
if (Directory.Exists(path))
|
||||
vulkanSdk = path;
|
||||
}
|
||||
|
||||
@@ -41,4 +41,15 @@
|
||||
#define VMA_NOT_NULL
|
||||
#include <ThirdParty/VulkanMemoryAllocator/vk_mem_alloc.h>
|
||||
|
||||
#if PLATFORM_APPLE_FAMILY
|
||||
// Declare potentially missing extensions from newer SDKs
|
||||
#ifndef VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME
|
||||
#define VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME "VK_KHR_portability_enumeration"
|
||||
#define VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR 0x00000001
|
||||
#endif
|
||||
#ifndef VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME
|
||||
#define VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME "VK_KHR_portability_subset"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -97,7 +97,8 @@ Action Input::MouseLeave;
|
||||
Delegate<const Float2&, int32> Input::TouchDown;
|
||||
Delegate<const Float2&, int32> Input::TouchMove;
|
||||
Delegate<const Float2&, int32> Input::TouchUp;
|
||||
Delegate<StringView> Input::ActionTriggered;
|
||||
Delegate<StringView, InputActionState> Input::ActionTriggered;
|
||||
Delegate<StringView> Input::AxisValueChanged;
|
||||
Array<ActionConfig> Input::ActionMappings;
|
||||
Array<AxisConfig> Input::AxisMappings;
|
||||
|
||||
@@ -1017,14 +1018,22 @@ void InputService::Update()
|
||||
Input::SetMousePosition(Screen::GetSize() * 0.5f);
|
||||
}
|
||||
|
||||
// Send events for the active actions (send events only in play mode)
|
||||
// Send events for the active actions and axes (send events only in play mode)
|
||||
if (!Time::GetGamePaused())
|
||||
{
|
||||
for (auto i = Axes.Begin(); i.IsNotEnd(); ++i)
|
||||
{
|
||||
if (Math::NotNearEqual(i->Value.Value, i->Value.PrevKeyValue))
|
||||
{
|
||||
Input::AxisValueChanged(i->Key);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto i = Actions.Begin(); i.IsNotEnd(); ++i)
|
||||
{
|
||||
if (i->Value.Active)
|
||||
if (i->Value.State != InputActionState::Waiting)
|
||||
{
|
||||
Input::ActionTriggered(i->Key);
|
||||
Input::ActionTriggered(i->Key, i->Value.State);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +293,13 @@ public:
|
||||
/// Event fired when virtual input action is triggered. Called before scripts update. See <see cref="ActionMappings"/> to edit configuration.
|
||||
/// </summary>
|
||||
/// <seealso cref="InputEvent"/>
|
||||
API_EVENT() static Delegate<StringView> ActionTriggered;
|
||||
API_EVENT() static Delegate<StringView, InputActionState> ActionTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when virtual input axis is changed. Called before scripts update. See <see cref="AxisMappings"/> to edit configuration.
|
||||
/// </summary>
|
||||
/// <seealso cref="InputAxis"/>
|
||||
API_EVENT() static Delegate<StringView> AxisValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the virtual action identified by name. Use <see cref="ActionMappings"/> to get the current config.
|
||||
|
||||
@@ -697,7 +697,8 @@ void AnimatedModel::UpdateBounds()
|
||||
}
|
||||
else if (model && model->IsLoaded() && model->LODs.Count() != 0)
|
||||
{
|
||||
BoundingBox box = model->LODs[0].GetBox(_transform, _deformation);
|
||||
const BoundingBox modelBox = model->GetBox(_transform.GetWorld());
|
||||
BoundingBox box = modelBox;
|
||||
if (GraphInstance.NodesPose.Count() != 0)
|
||||
{
|
||||
// Per-bone bounds estimated from positions
|
||||
@@ -705,11 +706,11 @@ void AnimatedModel::UpdateBounds()
|
||||
const int32 bonesCount = skeleton.Bones.Count();
|
||||
for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
|
||||
box.Merge(_transform.LocalToWorld(GraphInstance.NodesPose[skeleton.Bones.Get()[boneIndex].NodeIndex].GetTranslation()));
|
||||
_box = box;
|
||||
}
|
||||
_box = box;
|
||||
|
||||
// Apply margin based on model dimensions
|
||||
const Vector3 modelBoxSize = model->GetBox().GetSize();
|
||||
const Vector3 modelBoxSize = modelBox.GetSize();
|
||||
const Vector3 center = _box.GetCenter();
|
||||
const Vector3 sizeHalf = Vector3::Max(_box.GetSize() + modelBoxSize * 0.2f, modelBoxSize) * 0.5f;
|
||||
_box = BoundingBox(center - sizeHalf, center + sizeHalf);
|
||||
|
||||
65
Source/Engine/Level/Components/MissingScript.h
Normal file
65
Source/Engine/Level/Components/MissingScript.h
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
#include "Engine/Core/Cache.h"
|
||||
#include "Engine/Scripting/Script.h"
|
||||
#include "Engine/Scripting/ScriptingObjectReference.h"
|
||||
#include "Engine/Serialization/JsonWriters.h"
|
||||
|
||||
/// <summary>
|
||||
/// Actor script component that represents missing script.
|
||||
/// </summary>
|
||||
API_CLASS(Attributes="HideInEditor") class FLAXENGINE_API MissingScript : public Script
|
||||
{
|
||||
API_AUTO_SERIALIZATION();
|
||||
DECLARE_SCRIPTING_TYPE(MissingScript);
|
||||
|
||||
private:
|
||||
ScriptingObjectReference<Script> _referenceScript;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Namespace and type name of missing script.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="ReadOnly") String MissingTypeName;
|
||||
|
||||
/// <summary>
|
||||
/// Missing script serialized data.
|
||||
/// </summary>
|
||||
API_FIELD(Hidden, Attributes="HideInEditor") String Data;
|
||||
|
||||
/// <summary>
|
||||
/// Field for assigning new script to transfer data to.
|
||||
/// </summary>
|
||||
API_PROPERTY() ScriptingObjectReference<Script> GetReferenceScript() const
|
||||
{
|
||||
return _referenceScript;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Field for assigning new script to transfer data to.
|
||||
/// </summary>
|
||||
API_PROPERTY() void SetReferenceScript(const ScriptingObjectReference<Script>& value)
|
||||
{
|
||||
_referenceScript = value;
|
||||
if (Data.IsEmpty())
|
||||
return;
|
||||
rapidjson_flax::Document document;
|
||||
document.Parse(Data.ToStringAnsi().GetText());
|
||||
|
||||
auto modifier = Cache::ISerializeModifier.Get();
|
||||
_referenceScript->Deserialize(document, modifier.Value);
|
||||
|
||||
DeleteObject();
|
||||
}
|
||||
};
|
||||
|
||||
inline MissingScript::MissingScript(const SpawnParams& params)
|
||||
: Script(params)
|
||||
{
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "SceneObjectsFactory.h"
|
||||
#include "Components/MissingScript.h"
|
||||
#include "Engine/Level/Actor.h"
|
||||
#include "Engine/Level/Prefabs/Prefab.h"
|
||||
#include "Engine/Content/Content.h"
|
||||
@@ -257,7 +258,8 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::
|
||||
rapidjson_flax::StringBuffer buffer;
|
||||
PrettyJsonWriter writer(buffer);
|
||||
value.Accept(writer.GetWriter());
|
||||
LOG(Warning, "Failed to deserialize scene object from data: {0}", String(buffer.GetString()));
|
||||
String bufferStr(buffer.GetString());
|
||||
LOG(Warning, "Failed to deserialize scene object from data: {0}", bufferStr);
|
||||
|
||||
// Try to log some useful info about missing object (eg. it's parent name for faster fixing)
|
||||
const auto parentIdMember = value.FindMember("ParentID");
|
||||
@@ -267,6 +269,14 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::
|
||||
Actor* parent = Scripting::FindObject<Actor>(parentId);
|
||||
if (parent)
|
||||
{
|
||||
#if USE_EDITOR
|
||||
// Add dummy script
|
||||
auto* dummyScript = parent->AddScript<MissingScript>();
|
||||
const auto parentIdMember = value.FindMember("TypeName");
|
||||
if (parentIdMember != value.MemberEnd() && parentIdMember->value.IsString())
|
||||
dummyScript->MissingTypeName = parentIdMember->value.GetString();
|
||||
dummyScript->Data = MoveTemp(bufferStr);
|
||||
#endif
|
||||
LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,8 +1283,8 @@ void ParticlesSystem::Job(int32 index)
|
||||
updateBounds = true;
|
||||
}
|
||||
// TODO: if using fixed timestep quantize the dt and accumulate remaining part for the next update?
|
||||
if (dt <= 1.0f / 240.0f)
|
||||
return;
|
||||
//if (dt <= 1.0f / 240.0f)
|
||||
// return;
|
||||
dt *= effect->SimulationSpeed;
|
||||
instance.Time += dt;
|
||||
const float fps = particleSystem->FramesPerSecond;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/resource.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <uuid/uuid.h>
|
||||
@@ -316,6 +317,14 @@ bool ApplePlatform::Init()
|
||||
OnPlatformUserAdd(New<User>(username));
|
||||
}
|
||||
|
||||
// Increase the maximum number of simultaneously open files
|
||||
{
|
||||
struct rlimit limit;
|
||||
limit.rlim_cur = OPEN_MAX;
|
||||
limit.rlim_max = RLIM_INFINITY;
|
||||
setrlimit(RLIMIT_NOFILE, &limit);
|
||||
}
|
||||
|
||||
AutoreleasePool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
return false;
|
||||
|
||||
@@ -220,6 +220,12 @@ void WindowsWindow::Show()
|
||||
if (!_settings.HasBorder)
|
||||
{
|
||||
SetWindowPos(_handle, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
if (!_settings.IsRegularWindow && _settings.ShowAfterFirstPaint && _settings.StartPosition == WindowStartPosition::Manual)
|
||||
{
|
||||
int32 x = Math::TruncToInt(_settings.Position.X);
|
||||
int32 y = Math::TruncToInt(_settings.Position.Y);
|
||||
SetWindowPos(_handle, nullptr, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -711,8 +717,27 @@ void WindowsWindow::CheckForWindowResize()
|
||||
// Cache client size
|
||||
RECT rect;
|
||||
GetClientRect(_handle, &rect);
|
||||
const int32 width = Math::Max(rect.right - rect.left, 0L);
|
||||
const int32 height = Math::Max(rect.bottom - rect.top, 0L);
|
||||
int32 width = Math::Max(rect.right - rect.left, 0L);
|
||||
int32 height = Math::Max(rect.bottom - rect.top, 0L);
|
||||
|
||||
// Check for windows maximized size and see if it needs to adjust position if needed
|
||||
if (_maximized)
|
||||
{
|
||||
// Pick the current monitor data for sizing
|
||||
const HMONITOR monitor = MonitorFromWindow(_handle, MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO monitorInfo;
|
||||
monitorInfo.cbSize = sizeof(MONITORINFO);
|
||||
GetMonitorInfoW(monitor, &monitorInfo);
|
||||
|
||||
auto cwidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
|
||||
auto cheight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
|
||||
if (width > cwidth && height > cheight)
|
||||
{
|
||||
width = cwidth;
|
||||
height = cheight;
|
||||
SetWindowPos(_handle, HWND_TOP, monitorInfo.rcWork.left, monitorInfo.rcWork.top, width, height, SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
|
||||
}
|
||||
}
|
||||
_clientSize = Float2(static_cast<float>(width), static_cast<float>(height));
|
||||
|
||||
// Check if window size has been changed
|
||||
|
||||
@@ -378,6 +378,34 @@ namespace FlaxEngine.Json
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize <see cref="Tag"/> as inlined text.
|
||||
/// </summary>
|
||||
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
|
||||
internal class TagConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
|
||||
{
|
||||
var tag = (Tag)value;
|
||||
writer.WriteValue(tag.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
return Tags.Get((string)reader.Value);
|
||||
return Tag.Default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(Tag);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Serialize Guid values using `N` format
|
||||
|
||||
@@ -128,6 +128,7 @@ namespace FlaxEngine.Json
|
||||
settings.Converters.Add(new MarginConverter());
|
||||
settings.Converters.Add(new VersionConverter());
|
||||
settings.Converters.Add(new LocalizedStringConverter());
|
||||
settings.Converters.Add(new TagConverter());
|
||||
//settings.Converters.Add(new GuidConverter());
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ BoundingBox JsonTools::GetBoundingBox(const Value& value)
|
||||
|
||||
Guid JsonTools::GetGuid(const Value& value)
|
||||
{
|
||||
if (value.IsNull())
|
||||
if (!value.IsString())
|
||||
return Guid::Empty;
|
||||
CHECK_RETURN(value.GetStringLength() == 32, Guid::Empty);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Engine/Core/Math/Matrix.h"
|
||||
#include "Engine/Core/Collections/Dictionary.h"
|
||||
#include "Engine/Platform/FileSystem.h"
|
||||
#include "Engine/Platform/File.h"
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
|
||||
// Import Assimp library
|
||||
@@ -516,8 +517,47 @@ bool ImportTexture(ImportedModelData& result, AssimpImporterData& data, aiString
|
||||
bool ImportMaterialTexture(ImportedModelData& result, AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type)
|
||||
{
|
||||
aiString aFilename;
|
||||
return aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS &&
|
||||
ImportTexture(result, data, aFilename, textureIndex, type);
|
||||
if (aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS)
|
||||
{
|
||||
// Check for embedded textures
|
||||
String filename = String(aFilename.C_Str()).TrimTrailing();
|
||||
if (filename.StartsWith(TEXT(AI_EMBEDDED_TEXNAME_PREFIX)))
|
||||
{
|
||||
const aiTexture* aTex = data.Scene->GetEmbeddedTexture(aFilename.C_Str());
|
||||
const StringView texIndexName(filename.Get() + (ARRAY_COUNT(AI_EMBEDDED_TEXNAME_PREFIX) - 1));
|
||||
uint32 texIndex;
|
||||
if (!aTex && !StringUtils::Parse(texIndexName.Get(), texIndexName.Length(), &texIndex) && texIndex >= 0 && texIndex < data.Scene->mNumTextures)
|
||||
aTex = data.Scene->mTextures[texIndex];
|
||||
if (aTex && aTex->mHeight == 0 && aTex->mWidth > 0)
|
||||
{
|
||||
// Export embedded texture to temporary file
|
||||
filename = String::Format(TEXT("{0}_tex_{1}.{2}"), StringUtils::GetFileNameWithoutExtension(data.Path), texIndexName, String(aTex->achFormatHint));
|
||||
File::WriteAllBytes(String(StringUtils::GetDirectoryName(data.Path)) / filename, (const byte*)aTex->pcData, (int32)aTex->mWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Find texture file path
|
||||
String path;
|
||||
if (ModelTool::FindTexture(data.Path, filename, path))
|
||||
return true;
|
||||
|
||||
// Check if already used
|
||||
textureIndex = 0;
|
||||
while (textureIndex < result.Textures.Count())
|
||||
{
|
||||
if (result.Textures[textureIndex].FilePath == path)
|
||||
return true;
|
||||
textureIndex++;
|
||||
}
|
||||
|
||||
// Import texture
|
||||
auto& texture = result.Textures.AddOne();
|
||||
texture.FilePath = path;
|
||||
texture.Type = type;
|
||||
texture.AssetID = Guid::Empty;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ImportMaterials(ImportedModelData& result, AssimpImporterData& data, String& errorMsg)
|
||||
@@ -706,8 +746,15 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create root node
|
||||
AssimpNode& rootNode = context->Nodes.AddOne();
|
||||
rootNode.ParentIndex = -1;
|
||||
rootNode.LodIndex = 0;
|
||||
rootNode.Name = TEXT("Root");
|
||||
rootNode.LocalTransform = Transform::Identity;
|
||||
|
||||
// Process imported scene nodes
|
||||
ProcessNodes(*context, context->Scene->mRootNode, -1);
|
||||
ProcessNodes(*context, context->Scene->mRootNode, 0);
|
||||
}
|
||||
DeleteMe<AssimpImporterData> contextCleanup(options.SplitContext ? nullptr : context);
|
||||
|
||||
@@ -823,7 +870,13 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti
|
||||
const auto animations = context->Scene->mAnimations[animIndex];
|
||||
data.Animation.Channels.Resize(animations->mNumChannels, false);
|
||||
data.Animation.Duration = animations->mDuration;
|
||||
data.Animation.FramesPerSecond = animations->mTicksPerSecond != 0.0 ? animations->mTicksPerSecond : 25.0;
|
||||
data.Animation.FramesPerSecond = animations->mTicksPerSecond;
|
||||
if (data.Animation.FramesPerSecond <= 0)
|
||||
{
|
||||
data.Animation.FramesPerSecond = context->Options.DefaultFrameRate;
|
||||
if (data.Animation.FramesPerSecond <= 0)
|
||||
data.Animation.FramesPerSecond = 30.0f;
|
||||
}
|
||||
|
||||
for (unsigned i = 0; i < animations->mNumChannels; i++)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
#include "Engine/Platform/File.h"
|
||||
|
||||
#define OPEN_FBX_CONVERT_SPACE 1
|
||||
|
||||
// Import OpenFBX library
|
||||
// Source: https://github.com/nem0/OpenFBX
|
||||
#include <ThirdParty/OpenFBX/ofbx.h>
|
||||
@@ -85,12 +87,16 @@ struct OpenFbxImporterData
|
||||
const ModelTool::Options& Options;
|
||||
|
||||
ofbx::GlobalSettings GlobalSettings;
|
||||
#if OPEN_FBX_CONVERT_SPACE
|
||||
Quaternion RootConvertRotation = Quaternion::Identity;
|
||||
Float3 Up;
|
||||
Float3 Front;
|
||||
Float3 Right;
|
||||
bool ConvertRH;
|
||||
#else
|
||||
static constexpr bool ConvertRH = false;
|
||||
#endif
|
||||
float FrameRate;
|
||||
Quaternion RootConvertRotation = Quaternion::Identity;
|
||||
|
||||
Array<FbxNode> Nodes;
|
||||
Array<FbxBone> Bones;
|
||||
@@ -103,7 +109,9 @@ struct OpenFbxImporterData
|
||||
, Path(path)
|
||||
, Options(options)
|
||||
, GlobalSettings(*scene->getGlobalSettings())
|
||||
#if OPEN_FBX_CONVERT_SPACE
|
||||
, ConvertRH(GlobalSettings.CoordAxis == ofbx::CoordSystem_RightHanded)
|
||||
#endif
|
||||
, Nodes(static_cast<int32>(scene->getMeshCount() * 4.0f))
|
||||
{
|
||||
float frameRate = scene->getSceneFrameRate();
|
||||
@@ -114,6 +122,7 @@ struct OpenFbxImporterData
|
||||
frameRate = 30.0f;
|
||||
}
|
||||
FrameRate = frameRate;
|
||||
#if OPEN_FBX_CONVERT_SPACE
|
||||
const float coordAxisSign = GlobalSettings.CoordAxis == ofbx::CoordSystem_LeftHanded ? -1.0f : +1.0f;
|
||||
switch (GlobalSettings.UpAxis)
|
||||
{
|
||||
@@ -170,6 +179,7 @@ struct OpenFbxImporterData
|
||||
break;
|
||||
default: ;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ImportMaterialTexture(ImportedModelData& result, const ofbx::Material* mat, ofbx::Texture::TextureType textureType, int32& textureIndex, TextureEntry::TypeHint type) const
|
||||
@@ -366,13 +376,12 @@ void ProcessNodes(OpenFbxImporterData& data, const ofbx::Object* aNode, int32 pa
|
||||
{
|
||||
node.LodIndex = data.Nodes[parentIndex].LodIndex;
|
||||
if (node.LodIndex == 0)
|
||||
{
|
||||
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
|
||||
}
|
||||
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
|
||||
}
|
||||
|
||||
auto transform = ToMatrix(aNode->evalLocal(aNode->getLocalTranslation(), aNode->getLocalRotation()));
|
||||
#if OPEN_FBX_CONVERT_SPACE
|
||||
if (data.ConvertRH)
|
||||
{
|
||||
// Mirror all base vectors at the local Z axis
|
||||
@@ -388,6 +397,7 @@ void ProcessNodes(OpenFbxImporterData& data, const ofbx::Object* aNode, int32 pa
|
||||
transform.M33 = -transform.M33;
|
||||
transform.M43 = -transform.M43;
|
||||
}
|
||||
#endif
|
||||
transform.Decompose(node.LocalTransform);
|
||||
data.Nodes.Add(node);
|
||||
|
||||
@@ -416,8 +426,9 @@ Matrix GetOffsetMatrix(OpenFbxImporterData& data, const ofbx::Mesh* mesh, const
|
||||
}
|
||||
}
|
||||
}
|
||||
//return Matrix::Identity;
|
||||
return ToMatrix(node->getGlobalTransform());
|
||||
#elif 1
|
||||
#else
|
||||
Matrix t = Matrix::Identity;
|
||||
const int32 boneIdx = data.FindBone(node);
|
||||
int32 idx = data.Bones[boneIdx].NodeIndex;
|
||||
@@ -427,17 +438,6 @@ Matrix GetOffsetMatrix(OpenFbxImporterData& data, const ofbx::Mesh* mesh, const
|
||||
idx = data.Nodes[idx].ParentIndex;
|
||||
} while (idx != -1);
|
||||
return t;
|
||||
#else
|
||||
auto* skin = mesh->getGeometry()->getSkin();
|
||||
for (int i = 0, c = skin->getClusterCount(); i < c; i++)
|
||||
{
|
||||
const ofbx::Cluster* cluster = skin->getCluster(i);
|
||||
if (cluster->getLink() == node)
|
||||
{
|
||||
return ToMatrix(cluster->getTransformLinkMatrix());
|
||||
}
|
||||
}
|
||||
return Matrix::Identity;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -455,17 +455,14 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
|
||||
const auto aMesh = data.Scene->getMesh(i);
|
||||
const auto aGeometry = aMesh->getGeometry();
|
||||
const ofbx::Skin* skin = aGeometry->getSkin();
|
||||
|
||||
if (skin == nullptr || IsMeshInvalid(aMesh))
|
||||
continue;
|
||||
|
||||
for (int clusterIndex = 0, c = skin->getClusterCount(); clusterIndex < c; clusterIndex++)
|
||||
for (int clusterIndex = 0, clusterCount = skin->getClusterCount(); clusterIndex < clusterCount; clusterIndex++)
|
||||
{
|
||||
const ofbx::Cluster* cluster = skin->getCluster(clusterIndex);
|
||||
|
||||
if (cluster->getIndicesCount() == 0)
|
||||
continue;
|
||||
|
||||
const auto link = cluster->getLink();
|
||||
ASSERT(link != nullptr);
|
||||
|
||||
@@ -487,7 +484,7 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
|
||||
|
||||
// Add bone
|
||||
boneIndex = data.Bones.Count();
|
||||
data.Bones.EnsureCapacity(Math::Max(128, boneIndex + 16));
|
||||
data.Bones.EnsureCapacity(256);
|
||||
data.Bones.Resize(boneIndex + 1);
|
||||
auto& bone = data.Bones[boneIndex];
|
||||
|
||||
@@ -495,7 +492,7 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
|
||||
bone.NodeIndex = nodeIndex;
|
||||
bone.ParentBoneIndex = -1;
|
||||
bone.FbxObj = link;
|
||||
bone.OffsetMatrix = GetOffsetMatrix(data, aMesh, link);
|
||||
bone.OffsetMatrix = GetOffsetMatrix(data, aMesh, link) * Matrix::Scaling(data.GlobalSettings.UnitScaleFactor);
|
||||
bone.OffsetMatrix.Invert();
|
||||
|
||||
// Mirror offset matrices (RH to LH)
|
||||
@@ -509,6 +506,15 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg)
|
||||
m.M32 = -m.M32;
|
||||
m.M34 = -m.M34;
|
||||
}
|
||||
|
||||
// Convert bone matrix if scene uses root transform
|
||||
if (!data.RootConvertRotation.IsIdentity())
|
||||
{
|
||||
Matrix m;
|
||||
Matrix::RotationQuaternion(data.RootConvertRotation, m);
|
||||
m.Invert();
|
||||
bone.OffsetMatrix = m * bone.OffsetMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,9 +553,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
// Vertex positions
|
||||
mesh.Positions.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Positions.Get()[i] = ToFloat3(vertices[i + firstVertexOffset]);
|
||||
}
|
||||
|
||||
// Indices (dummy index buffer)
|
||||
if (vertexCount % 3 != 0)
|
||||
@@ -559,24 +563,18 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
}
|
||||
mesh.Indices.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Indices.Get()[i] = i;
|
||||
}
|
||||
|
||||
// Texture coordinates
|
||||
if (uvs)
|
||||
{
|
||||
mesh.UVs.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.UVs.Get()[i] = ToFloat2(uvs[i + firstVertexOffset]);
|
||||
}
|
||||
if (data.ConvertRH)
|
||||
{
|
||||
for (int32 v = 0; v < vertexCount; v++)
|
||||
{
|
||||
mesh.UVs[v].Y = 1.0f - mesh.UVs[v].Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,16 +591,12 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
{
|
||||
mesh.Normals.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Normals.Get()[i] = ToFloat3(normals[i + firstVertexOffset]);
|
||||
}
|
||||
if (data.ConvertRH)
|
||||
{
|
||||
// Mirror normals along the Z axis
|
||||
for (int32 i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Normals.Get()[i].Z *= -1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,16 +609,12 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
{
|
||||
mesh.Tangents.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Tangents.Get()[i] = ToFloat3(tangents[i + firstVertexOffset]);
|
||||
}
|
||||
if (data.ConvertRH)
|
||||
{
|
||||
// Mirror tangents along the Z axis
|
||||
for (int32 i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Tangents.Get()[i].Z *= -1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,15 +660,11 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
{
|
||||
mesh.LightmapUVs.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.LightmapUVs.Get()[i] = ToFloat2(lightmapUVs[i + firstVertexOffset]);
|
||||
}
|
||||
if (data.ConvertRH)
|
||||
{
|
||||
for (int32 v = 0; v < vertexCount; v++)
|
||||
{
|
||||
mesh.LightmapUVs[v].Y = 1.0f - mesh.LightmapUVs[v].Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -692,9 +678,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
{
|
||||
mesh.Colors.Resize(vertexCount, false);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Colors.Get()[i] = ToColor(colors[i + firstVertexOffset]);
|
||||
}
|
||||
}
|
||||
|
||||
// Blend Indices and Blend Weights
|
||||
@@ -705,13 +689,11 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
mesh.BlendIndices.SetAll(Int4::Zero);
|
||||
mesh.BlendWeights.SetAll(Float4::Zero);
|
||||
|
||||
for (int clusterIndex = 0, c = skin->getClusterCount(); clusterIndex < c; clusterIndex++)
|
||||
for (int clusterIndex = 0, clusterCount = skin->getClusterCount(); clusterIndex < clusterCount; clusterIndex++)
|
||||
{
|
||||
const ofbx::Cluster* cluster = skin->getCluster(clusterIndex);
|
||||
|
||||
if (cluster->getIndicesCount() == 0)
|
||||
continue;
|
||||
|
||||
const auto link = cluster->getLink();
|
||||
ASSERT(link != nullptr);
|
||||
|
||||
@@ -815,15 +797,11 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
{
|
||||
// Mirror positions along the Z axis
|
||||
for (int32 i = 0; i < vertexCount; i++)
|
||||
{
|
||||
mesh.Positions[i].Z *= -1.0f;
|
||||
}
|
||||
for (auto& blendShapeData : mesh.BlendShapes)
|
||||
{
|
||||
for (auto& v : blendShapeData.Vertices)
|
||||
{
|
||||
v.PositionDelta.Z *= -1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,9 +812,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb
|
||||
{
|
||||
// Invert the order
|
||||
for (int32 i = 0; i < mesh.Indices.Count(); i += 3)
|
||||
{
|
||||
Swap(mesh.Indices[i], mesh.Indices[i + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
if ((data.Options.CalculateTangents || !tangents) && mesh.UVs.HasItems())
|
||||
@@ -888,9 +864,7 @@ bool ImportMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx
|
||||
{
|
||||
node.LodIndex = data.Nodes[0].LodIndex;
|
||||
if (node.LodIndex == 0)
|
||||
{
|
||||
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
|
||||
}
|
||||
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
|
||||
}
|
||||
node.LocalTransform = Transform::Identity;
|
||||
@@ -999,7 +973,6 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve<T>& curv
|
||||
{
|
||||
if (curveNode == nullptr)
|
||||
return;
|
||||
|
||||
const auto keyframes = curve.Resize(info.FramesCount);
|
||||
const auto bone = curveNode->getBone();
|
||||
Frame localFrame;
|
||||
@@ -1143,6 +1116,14 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
|
||||
}
|
||||
fileData.Resize(0);
|
||||
|
||||
// Tweak scene if exported by Blender
|
||||
auto& globalInfo = *scene->getGlobalInfo();
|
||||
if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase))
|
||||
{
|
||||
auto ptr = const_cast<ofbx::GlobalSettings*>(scene->getGlobalSettings());
|
||||
ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1);
|
||||
}
|
||||
|
||||
// Process imported scene
|
||||
context = New<OpenFbxImporterData>(path, options, scene);
|
||||
auto& globalSettings = context->GlobalSettings;
|
||||
@@ -1153,10 +1134,13 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
|
||||
|
||||
// Log scene info
|
||||
LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context->FrameRate, globalSettings.UnitScaleFactor);
|
||||
LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor));
|
||||
LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-"));
|
||||
LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-"));
|
||||
LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)"));
|
||||
#if OPEN_FBX_CONVERT_SPACE
|
||||
LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context->Up, context->Front, context->Right);
|
||||
#endif
|
||||
|
||||
// Extract embedded textures
|
||||
if (EnumHasAnyFlags(data.Types, ImportDataTypes::Textures))
|
||||
@@ -1186,6 +1170,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
|
||||
}
|
||||
}
|
||||
|
||||
#if OPEN_FBX_CONVERT_SPACE
|
||||
// Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded))
|
||||
if (context->Up == Float3(1, 0, 0) && context->Front == Float3(0, 0, 1) && context->Right == Float3(0, 1, 0))
|
||||
{
|
||||
@@ -1215,15 +1200,16 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt
|
||||
context->RootConvertRotation = Quaternion::Euler(90, 0, 0);*/
|
||||
if (!context->RootConvertRotation.IsIdentity())
|
||||
{
|
||||
for (int32 i = 0; i < context->Nodes.Count(); i++)
|
||||
for (auto& node : context->Nodes)
|
||||
{
|
||||
if (context->Nodes[i].ParentIndex == -1)
|
||||
if (node.ParentIndex == -1)
|
||||
{
|
||||
context->Nodes[i].LocalTransform.Orientation = context->RootConvertRotation * context->Nodes[i].LocalTransform.Orientation;
|
||||
node.LocalTransform.Orientation = context->RootConvertRotation * node.LocalTransform.Orientation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
DeleteMe<OpenFbxImporterData> contextCleanup(options.SplitContext ? nullptr : context);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "Engine/Tools/TextureTool/TextureTool.h"
|
||||
#include "Engine/ContentImporters/AssetsImportingManager.h"
|
||||
#include "Engine/ContentImporters/CreateMaterial.h"
|
||||
#include "Engine/ContentImporters/CreateMaterialInstance.h"
|
||||
#include "Engine/ContentImporters/CreateCollisionData.h"
|
||||
#include "Engine/Serialization/Serialization.h"
|
||||
#include "Editor/Utilities/EditorUtilities.h"
|
||||
@@ -387,6 +388,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
|
||||
SERIALIZE(SloppyOptimization);
|
||||
SERIALIZE(LODTargetError);
|
||||
SERIALIZE(ImportMaterials);
|
||||
SERIALIZE(ImportMaterialsAsInstances);
|
||||
SERIALIZE(InstanceToImportAs);
|
||||
SERIALIZE(ImportTextures);
|
||||
SERIALIZE(RestoreMaterialsOnReimport);
|
||||
SERIALIZE(GenerateSDF);
|
||||
@@ -430,6 +433,8 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
|
||||
DESERIALIZE(SloppyOptimization);
|
||||
DESERIALIZE(LODTargetError);
|
||||
DESERIALIZE(ImportMaterials);
|
||||
DESERIALIZE(ImportMaterialsAsInstances);
|
||||
DESERIALIZE(InstanceToImportAs);
|
||||
DESERIALIZE(ImportTextures);
|
||||
DESERIALIZE(RestoreMaterialsOnReimport);
|
||||
DESERIALIZE(GenerateSDF);
|
||||
@@ -503,11 +508,11 @@ bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options&
|
||||
if (ImportDataAssimp(importPath.Get(), data, options, errorMsg))
|
||||
return true;
|
||||
#elif USE_AUTODESK_FBX_SDK
|
||||
if (ImportDataAutodeskFbxSdk(importPath.Get(), data, options, errorMsg))
|
||||
return true;
|
||||
if (ImportDataAutodeskFbxSdk(importPath.Get(), data, options, errorMsg))
|
||||
return true;
|
||||
#elif USE_OPEN_FBX
|
||||
if (ImportDataOpenFBX(importPath.Get(), data, options, errorMsg))
|
||||
return true;
|
||||
if (ImportDataOpenFBX(importPath.Get(), data, options, errorMsg))
|
||||
return true;
|
||||
#else
|
||||
LOG(Error, "Compiled without model importing backend.");
|
||||
return true;
|
||||
@@ -620,62 +625,62 @@ bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options&
|
||||
|
||||
bool SortDepths(const Pair<int32, int32>& a, const Pair<int32, int32>& b)
|
||||
{
|
||||
return a.First < b.First;
|
||||
return a.First < b.First;
|
||||
}
|
||||
|
||||
void CreateLinearListFromTree(Array<SkeletonNode>& nodes, Array<int32>& mapping)
|
||||
{
|
||||
// Customized breadth first tree algorithm (each node has no direct reference to the children so we build the cache for the nodes depth level)
|
||||
const int32 count = nodes.Count();
|
||||
Array<Pair<int32, int32>> depths(count); // Pair.First = Depth, Pair.Second = Node Index
|
||||
depths.SetSize(count);
|
||||
depths.Set(-1);
|
||||
for (int32 i = 0; i < count; i++)
|
||||
{
|
||||
// Skip evaluated nodes
|
||||
if (depths[i].First != -1)
|
||||
continue;
|
||||
const int32 count = nodes.Count();
|
||||
Array<Pair<int32, int32>> depths(count); // Pair.First = Depth, Pair.Second = Node Index
|
||||
depths.Resize(count);
|
||||
depths.SetAll(-1);
|
||||
for (int32 i = 0; i < count; i++)
|
||||
{
|
||||
// Skip evaluated nodes
|
||||
if (depths[i].First != -1)
|
||||
continue;
|
||||
|
||||
// Find the first node with calculated depth and get the distance to it
|
||||
int32 end = i;
|
||||
int32 lastDepth;
|
||||
int32 relativeDepth = 0;
|
||||
do
|
||||
{
|
||||
lastDepth = depths[end].First;
|
||||
end = nodes[end].ParentIndex;
|
||||
relativeDepth++;
|
||||
} while (end != -1 && lastDepth == -1);
|
||||
// Find the first node with calculated depth and get the distance to it
|
||||
int32 end = i;
|
||||
int32 lastDepth;
|
||||
int32 relativeDepth = 0;
|
||||
do
|
||||
{
|
||||
lastDepth = depths[end].First;
|
||||
end = nodes[end].ParentIndex;
|
||||
relativeDepth++;
|
||||
} while (end != -1 && lastDepth == -1);
|
||||
|
||||
// Set the depth (second item is the node index)
|
||||
depths[i] = MakePair(lastDepth + relativeDepth, i);
|
||||
}
|
||||
for (int32 i = 0; i < count; i++)
|
||||
{
|
||||
// Strange divide by 2 but works
|
||||
depths[i].First = depths[i].First >> 1;
|
||||
}
|
||||
// Set the depth (second item is the node index)
|
||||
depths[i] = ToPair(lastDepth + relativeDepth, i);
|
||||
}
|
||||
for (int32 i = 0; i < count; i++)
|
||||
{
|
||||
// Strange divide by 2 but works
|
||||
depths[i].First = depths[i].First >> 1;
|
||||
}
|
||||
|
||||
// Order nodes by depth O(n*log(n))
|
||||
depths.Sort(SortDepths);
|
||||
// Order nodes by depth O(n*log(n))
|
||||
depths.Sort(SortDepths);
|
||||
|
||||
// Extract nodes mapping O(n^2)
|
||||
mapping.EnsureCapacity(count, false);
|
||||
mapping.SetSize(count);
|
||||
for (int32 i = 0; i < count; i++)
|
||||
{
|
||||
int32 newIndex = -1;
|
||||
for (int32 j = 0; j < count; j++)
|
||||
{
|
||||
if (depths[j].Second == i)
|
||||
{
|
||||
newIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT(newIndex != -1);
|
||||
mapping[i] = newIndex;
|
||||
}
|
||||
// Extract nodes mapping O(n^2)
|
||||
mapping.EnsureCapacity(count, false);
|
||||
mapping.Resize(count);
|
||||
for (int32 i = 0; i < count; i++)
|
||||
{
|
||||
int32 newIndex = -1;
|
||||
for (int32 j = 0; j < count; j++)
|
||||
{
|
||||
if (depths[j].Second == i)
|
||||
{
|
||||
newIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT(newIndex != -1);
|
||||
mapping[i] = newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -841,19 +846,35 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
const auto mesh = data.LODs[0].Meshes[i];
|
||||
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
|
||||
{
|
||||
LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name);
|
||||
|
||||
auto indices = Int4::Zero;
|
||||
auto weights = Float4::UnitX;
|
||||
|
||||
// Check if use a single bone for skinning
|
||||
auto nodeIndex = data.Skeleton.FindNode(mesh->Name);
|
||||
auto boneIndex = data.Skeleton.FindBone(nodeIndex);
|
||||
if (boneIndex != -1)
|
||||
if (boneIndex == -1 && nodeIndex != -1 && data.Skeleton.Bones.Count() < MAX_BONES_PER_MODEL)
|
||||
{
|
||||
LOG(Warning, "Using auto-detected bone {0} (index {1})", data.Skeleton.Nodes[nodeIndex].Name, boneIndex);
|
||||
// Add missing bone to be used by skinned model from animated nodes pose
|
||||
boneIndex = data.Skeleton.Bones.Count();
|
||||
auto& bone = data.Skeleton.Bones.AddOne();
|
||||
bone.ParentIndex = -1;
|
||||
bone.NodeIndex = nodeIndex;
|
||||
bone.LocalTransform = CombineTransformsFromNodeIndices(data.Nodes, -1, nodeIndex);
|
||||
CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex);
|
||||
LOG(Warning, "Using auto-created bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
|
||||
indices.X = boneIndex;
|
||||
}
|
||||
else if (boneIndex != -1)
|
||||
{
|
||||
// Fallback to already added bone
|
||||
LOG(Warning, "Using auto-detected bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name);
|
||||
indices.X = boneIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No bone
|
||||
LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name);
|
||||
}
|
||||
|
||||
mesh->BlendIndices.Resize(mesh->Positions.Count());
|
||||
mesh->BlendWeights.Resize(mesh->Positions.Count());
|
||||
@@ -978,23 +999,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
importedFileNames.Add(filename);
|
||||
#if COMPILE_WITH_ASSETS_IMPORTER
|
||||
auto assetPath = autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT;
|
||||
CreateMaterial::Options materialOptions;
|
||||
materialOptions.Diffuse.Color = material.Diffuse.Color;
|
||||
if (material.Diffuse.TextureIndex != -1)
|
||||
materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID;
|
||||
materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask;
|
||||
materialOptions.Emissive.Color = material.Emissive.Color;
|
||||
if (material.Emissive.TextureIndex != -1)
|
||||
materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID;
|
||||
materialOptions.Opacity.Value = material.Opacity.Value;
|
||||
if (material.Opacity.TextureIndex != -1)
|
||||
materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID;
|
||||
if (material.Normals.TextureIndex != -1)
|
||||
materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID;
|
||||
if (material.TwoSided || material.Diffuse.HasAlphaMask)
|
||||
materialOptions.Info.CullMode = CullMode::TwoSided;
|
||||
if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1)
|
||||
materialOptions.Info.BlendMode = MaterialBlendMode::Transparent;
|
||||
|
||||
// When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1])
|
||||
if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1)
|
||||
@@ -1006,7 +1010,42 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
continue;
|
||||
}
|
||||
|
||||
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions);
|
||||
if (options.ImportMaterialsAsInstances)
|
||||
{
|
||||
// Create material instance
|
||||
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID);
|
||||
if (MaterialInstance* materialInstance = Content::Load<MaterialInstance>(assetPath))
|
||||
{
|
||||
materialInstance->SetBaseMaterial(options.InstanceToImportAs);
|
||||
materialInstance->Save();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(Error, "Failed to load material instance after creation. ({0})", assetPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create material
|
||||
CreateMaterial::Options materialOptions;
|
||||
materialOptions.Diffuse.Color = material.Diffuse.Color;
|
||||
if (material.Diffuse.TextureIndex != -1)
|
||||
materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID;
|
||||
materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask;
|
||||
materialOptions.Emissive.Color = material.Emissive.Color;
|
||||
if (material.Emissive.TextureIndex != -1)
|
||||
materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID;
|
||||
materialOptions.Opacity.Value = material.Opacity.Value;
|
||||
if (material.Opacity.TextureIndex != -1)
|
||||
materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID;
|
||||
if (material.Normals.TextureIndex != -1)
|
||||
materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID;
|
||||
if (material.TwoSided || material.Diffuse.HasAlphaMask)
|
||||
materialOptions.Info.CullMode = CullMode::TwoSided;
|
||||
if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1)
|
||||
materialOptions.Info.BlendMode = MaterialBlendMode::Transparent;
|
||||
AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1091,7 +1130,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
#if COMPILE_WITH_PHYSICS_COOKING
|
||||
// Create collision
|
||||
CollisionCooking::Argument arg;
|
||||
arg.Type = CollisionDataType::TriangleMesh;
|
||||
arg.Type = options.CollisionType;
|
||||
arg.OverrideModelData = &collisionModel;
|
||||
auto assetPath = autoImportOutput / StringUtils::GetFileNameWithoutExtension(path) + TEXT("Collision") ASSET_FILES_EXTENSION_WITH_DOT;
|
||||
if (CreateCollisionData::CookMeshCollision(assetPath, arg))
|
||||
@@ -1320,14 +1359,17 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
}
|
||||
}
|
||||
|
||||
// Apply import transform on root bones
|
||||
for (int32 i = 0; i < data.Skeleton.Bones.Count(); i++)
|
||||
// Apply import transform on bones
|
||||
Matrix importMatrixInv;
|
||||
importTransform.GetWorld(importMatrixInv);
|
||||
importMatrixInv.Invert();
|
||||
for (SkeletonBone& bone : data.Skeleton.Bones)
|
||||
{
|
||||
auto& bone = data.Skeleton.Bones.Get()[i];
|
||||
if (bone.ParentIndex == -1)
|
||||
{
|
||||
bone.LocalTransform = importTransform.LocalToWorld(bone.LocalTransform);
|
||||
}
|
||||
bone.OffsetMatrix = importMatrixInv * bone.OffsetMatrix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1363,41 +1405,32 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
// use SkeletonMapping<SkeletonBone> to map bones?
|
||||
|
||||
// Calculate offset matrix (inverse bind pose transform) for every bone manually
|
||||
/*for (SkeletonBone& bone : data.Skeleton.Bones)
|
||||
{
|
||||
for (int32 i = 0; i < data.Skeleton.Bones.Count(); i++)
|
||||
{
|
||||
Matrix t = Matrix::Identity;
|
||||
int32 idx = data.Skeleton.Bones[i].NodeIndex;
|
||||
do
|
||||
{
|
||||
t *= data.Skeleton.Nodes[idx].LocalTransform.GetWorld();
|
||||
idx = data.Skeleton.Nodes[idx].ParentIndex;
|
||||
} while (idx != -1);
|
||||
t.Invert();
|
||||
data.Skeleton.Bones[i].OffsetMatrix = t;
|
||||
}
|
||||
}
|
||||
CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex);
|
||||
}*/
|
||||
|
||||
#if USE_SKELETON_NODES_SORTING
|
||||
// Sort skeleton nodes and bones hierarchy (parents first)
|
||||
// Then it can be used with a simple linear loop update
|
||||
{
|
||||
const int32 nodesCount = data.Skeleton.Nodes.Count();
|
||||
const int32 bonesCount = data.Skeleton.Bones.Count();
|
||||
Array<int32> mapping;
|
||||
CreateLinearListFromTree(data.Skeleton.Nodes, mapping);
|
||||
for (int32 i = 0; i < nodesCount; i++)
|
||||
{
|
||||
auto& node = data.Skeleton.Nodes[i];
|
||||
node.ParentIndex = mapping[node.ParentIndex];
|
||||
}
|
||||
for (int32 i = 0; i < bonesCount; i++)
|
||||
{
|
||||
auto& bone = data.Skeleton.Bones[i];
|
||||
bone.NodeIndex = mapping[bone.NodeIndex];
|
||||
}
|
||||
}
|
||||
reorder_nodes_and_test_it_out!
|
||||
// Then it can be used with a simple linear loop update
|
||||
{
|
||||
const int32 nodesCount = data.Skeleton.Nodes.Count();
|
||||
const int32 bonesCount = data.Skeleton.Bones.Count();
|
||||
Array<int32> mapping;
|
||||
CreateLinearListFromTree(data.Skeleton.Nodes, mapping);
|
||||
for (int32 i = 0; i < nodesCount; i++)
|
||||
{
|
||||
auto& node = data.Skeleton.Nodes[i];
|
||||
node.ParentIndex = mapping[node.ParentIndex];
|
||||
}
|
||||
for (int32 i = 0; i < bonesCount; i++)
|
||||
{
|
||||
auto& bone = data.Skeleton.Bones[i];
|
||||
bone.NodeIndex = mapping[bone.NodeIndex];
|
||||
}
|
||||
}
|
||||
reorder_nodes_and_test_it_out
|
||||
!
|
||||
#endif
|
||||
}
|
||||
else if (options.Type == ModelType::Animation)
|
||||
@@ -1474,7 +1507,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
|
||||
|
||||
// Group meshes that can be merged together
|
||||
typedef Pair<int32, int32> MeshGroupKey;
|
||||
const std::function<MeshGroupKey(MeshData* const&)> f = [](MeshData* const& x) -> MeshGroupKey
|
||||
const Function<MeshGroupKey(MeshData* const&)> f = [](MeshData* const& x) -> MeshGroupKey
|
||||
{
|
||||
return MeshGroupKey(x->NodeIndex, x->MaterialSlotIndex);
|
||||
};
|
||||
@@ -1753,6 +1786,19 @@ bool ModelTool::FindTexture(const String& sourcePath, const String& file, String
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModelTool::CalculateBoneOffsetMatrix(const Array<SkeletonNode>& nodes, Matrix& offsetMatrix, int32 nodeIndex)
|
||||
{
|
||||
offsetMatrix = Matrix::Identity;
|
||||
int32 idx = nodeIndex;
|
||||
do
|
||||
{
|
||||
const SkeletonNode& node = nodes[idx];
|
||||
offsetMatrix *= node.LocalTransform.GetWorld();
|
||||
idx = node.ParentIndex;
|
||||
} while (idx != -1);
|
||||
offsetMatrix.Invert();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
#include "Engine/Core/Config.h"
|
||||
#include "Engine/Content/Assets/ModelBase.h"
|
||||
#include "Engine/Physics/CollisionData.h"
|
||||
#if USE_EDITOR
|
||||
#include "Engine/Core/ISerializable.h"
|
||||
#include "Engine/Graphics/Models/ModelData.h"
|
||||
#include "Engine/Graphics/Models/SkeletonData.h"
|
||||
#include "Engine/Animations/AnimationData.h"
|
||||
#include "Engine/Content/Assets/MaterialBase.h"
|
||||
|
||||
class JsonWriter;
|
||||
|
||||
@@ -262,6 +264,9 @@ public:
|
||||
// If specified, all meshes which name starts with this prefix will be imported as a separate collision data (excluded used for rendering).
|
||||
API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
|
||||
String CollisionMeshesPrefix = TEXT("");
|
||||
// The type of collision that should be generated if has collision prefix specified.
|
||||
API_FIELD(Attributes = "EditorOrder(105), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))")
|
||||
CollisionDataType CollisionType = CollisionDataType::TriangleMesh;
|
||||
|
||||
public: // Transform
|
||||
|
||||
@@ -334,11 +339,17 @@ public:
|
||||
// If checked, the importer will create materials for model meshes as specified in the file.
|
||||
API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
|
||||
bool ImportMaterials = true;
|
||||
// If checked, the importer will create the model's materials as instances of a base material.
|
||||
API_FIELD(Attributes = "EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials))")
|
||||
bool ImportMaterialsAsInstances = false;
|
||||
// The material to import the model's materials as an instance of.
|
||||
API_FIELD(Attributes = "EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances))")
|
||||
AssetReference<MaterialBase> InstanceToImportAs;
|
||||
// If checked, the importer will import texture files used by the model and any embedded texture resources.
|
||||
API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
|
||||
bool ImportTextures = true;
|
||||
// If checked, the importer will try to restore the model material slots.
|
||||
API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Restore Materials On Reimport\"), VisibleIf(nameof(ShowGeometry))")
|
||||
// If checked, the importer will try to keep the model's current material slots, instead of importing materials from the source file.
|
||||
API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Material Slots on Reimport\"), VisibleIf(nameof(ShowGeometry))")
|
||||
bool RestoreMaterialsOnReimport = true;
|
||||
|
||||
public: // SDF
|
||||
@@ -419,6 +430,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static void CalculateBoneOffsetMatrix(const Array<SkeletonNode>& nodes, Matrix& offsetMatrix, int32 nodeIndex);
|
||||
#if USE_ASSIMP
|
||||
static bool ImportDataAssimp(const char* path, ImportedModelData& data, Options& options, String& errorMsg);
|
||||
#endif
|
||||
|
||||
16
Source/Platforms/Mac/Default.entitlements
Normal file
16
Source/Platforms/Mac/Default.entitlements
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Source/Platforms/Mac/Default.icns
Normal file
BIN
Source/Platforms/Mac/Default.icns
Normal file
Binary file not shown.
43
Source/Platforms/Mac/Default.plist
Normal file
43
Source/Platforms/Mac/Default.plist
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon.icns</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{Executable}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>FlaxEditor</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.flaxengine.editor</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{Version}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{Version}</string>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<false/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{Copyright}</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.developer-tools</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15</string>
|
||||
<key>LSMinimumSystemVersionByArchitecture</key>
|
||||
<dict>
|
||||
<key>{Arch}</key>
|
||||
<string>10.15</string>
|
||||
</dict>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
167
Source/ThirdParty/OpenFBX/ofbx.cpp
vendored
167
Source/ThirdParty/OpenFBX/ofbx.cpp
vendored
@@ -365,6 +365,7 @@ float DataView::toFloat() const
|
||||
|
||||
bool DataView::operator==(const char* rhs) const
|
||||
{
|
||||
if (!begin) return !rhs[0];
|
||||
const char* c = rhs;
|
||||
const char* c2 = (const char*)begin;
|
||||
while (*c && c2 != (const char*)end)
|
||||
@@ -802,6 +803,14 @@ static OptionalError<Property*> readTextProperty(Cursor* cursor, Allocator& allo
|
||||
|
||||
prop->value.end = cursor->current;
|
||||
}
|
||||
else if (cursor->current < cursor->end && (*cursor->current == 'e' || *cursor->current == 'E')) {
|
||||
prop->type = 'D';
|
||||
// 10e-013
|
||||
++cursor->current;
|
||||
if (cursor->current < cursor->end && *cursor->current == '-') ++cursor->current;
|
||||
while (cursor->current < cursor->end && isdigit(*cursor->current)) ++cursor->current;
|
||||
prop->value.end = cursor->current;
|
||||
}
|
||||
return prop;
|
||||
}
|
||||
|
||||
@@ -1184,7 +1193,6 @@ struct GeometryImpl : Geometry
|
||||
const BlendShape* blendShape = nullptr;
|
||||
|
||||
std::vector<int> indices;
|
||||
std::vector<int> to_old_vertices;
|
||||
std::vector<NewVertex> to_new_vertices;
|
||||
|
||||
GeometryImpl(const Scene& _scene, const IElement& _element)
|
||||
@@ -1559,6 +1567,7 @@ struct Scene : IScene
|
||||
int getAnimationStackCount() const override { return (int)m_animation_stacks.size(); }
|
||||
int getMeshCount() const override { return (int)m_meshes.size(); }
|
||||
float getSceneFrameRate() const override { return m_scene_frame_rate; }
|
||||
const GlobalInfo* getGlobalInfo() const override { return &m_info; }
|
||||
const GlobalSettings* getGlobalSettings() const override { return &m_settings; }
|
||||
|
||||
const Object* const* getAllObjects() const override { return m_all_objects.empty() ? nullptr : &m_all_objects[0]; }
|
||||
@@ -1621,6 +1630,7 @@ struct Scene : IScene
|
||||
Element* m_root_element = nullptr;
|
||||
Root* m_root = nullptr;
|
||||
float m_scene_frame_rate = -1;
|
||||
GlobalInfo m_info;
|
||||
GlobalSettings m_settings;
|
||||
std::unordered_map<u64, ObjectPair> m_object_map;
|
||||
std::vector<Object*> m_all_objects;
|
||||
@@ -1765,7 +1775,7 @@ struct AnimationLayerImpl : AnimationLayer
|
||||
{
|
||||
for (const AnimationCurveNodeImpl* node : curve_nodes)
|
||||
{
|
||||
if (node->bone_link_property == prop && node->bone == &bone) return node;
|
||||
if (node->bone_link_property.begin && node->bone_link_property == prop && node->bone == &bone) return node;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -2474,6 +2484,15 @@ static void triangulate(
|
||||
++in_polygon_idx;
|
||||
if (old_indices[i] < 0)
|
||||
{
|
||||
if (in_polygon_idx <= 2) {
|
||||
// invalid polygon, let's pop it
|
||||
to_old_vertices->pop_back();
|
||||
to_old_indices->pop_back();
|
||||
if (in_polygon_idx == 2) {
|
||||
to_old_vertices->pop_back();
|
||||
to_old_indices->pop_back();
|
||||
}
|
||||
}
|
||||
in_polygon_idx = 0;
|
||||
}
|
||||
}
|
||||
@@ -2487,20 +2506,22 @@ static void buildGeometryVertexData(
|
||||
std::vector<int>& to_old_indices,
|
||||
bool triangulationEnabled)
|
||||
{
|
||||
std::vector<int> to_old_vertices;
|
||||
|
||||
if (triangulationEnabled) {
|
||||
triangulate(original_indices, &geom->to_old_vertices, &to_old_indices);
|
||||
geom->vertices.resize(geom->to_old_vertices.size());
|
||||
triangulate(original_indices, &to_old_vertices, &to_old_indices);
|
||||
geom->vertices.resize(to_old_vertices.size());
|
||||
geom->indices.resize(geom->vertices.size());
|
||||
for (int i = 0, c = (int)geom->to_old_vertices.size(); i < c; ++i)
|
||||
for (int i = 0, c = (int)to_old_vertices.size(); i < c; ++i)
|
||||
{
|
||||
geom->vertices[i] = vertices[geom->to_old_vertices[i]];
|
||||
geom->vertices[i] = vertices[to_old_vertices[i]];
|
||||
geom->indices[i] = codeIndex(i, i % 3 == 2);
|
||||
}
|
||||
} else {
|
||||
geom->vertices = vertices;
|
||||
geom->to_old_vertices.resize(original_indices.size());
|
||||
to_old_vertices.resize(original_indices.size());
|
||||
for (size_t i = 0; i < original_indices.size(); ++i) {
|
||||
geom->to_old_vertices[i] = decodeIndex(original_indices[i]);
|
||||
to_old_vertices[i] = decodeIndex(original_indices[i]);
|
||||
}
|
||||
geom->indices = original_indices;
|
||||
to_old_indices.resize(original_indices.size());
|
||||
@@ -2508,8 +2529,7 @@ static void buildGeometryVertexData(
|
||||
}
|
||||
|
||||
geom->to_new_vertices.resize(vertices.size()); // some vertices can be unused, so this isn't necessarily the same size as to_old_vertices.
|
||||
const int* to_old_vertices = geom->to_old_vertices.empty() ? nullptr : &geom->to_old_vertices[0];
|
||||
for (int i = 0, c = (int)geom->to_old_vertices.size(); i < c; ++i)
|
||||
for (int i = 0, c = (int)to_old_vertices.size(); i < c; ++i)
|
||||
{
|
||||
int old = to_old_vertices[i];
|
||||
add(geom->to_new_vertices[old], i);
|
||||
@@ -2736,7 +2756,7 @@ bool ShapeImpl::postprocess(GeometryImpl* geom, Allocator& allocator)
|
||||
allocator.vec3_tmp2.clear(); // old normals
|
||||
allocator.int_tmp.clear(); // old indices
|
||||
if (!parseDoubleVecData(*vertices_element->first_property, &allocator.vec3_tmp, &allocator.tmp)) return true;
|
||||
if (!parseDoubleVecData(*normals_element->first_property, &allocator.vec3_tmp2, &allocator.tmp)) return true;
|
||||
if (normals_element && !parseDoubleVecData(*normals_element->first_property, &allocator.vec3_tmp2, &allocator.tmp)) return true;
|
||||
if (!parseBinaryArray(*indexes_element->first_property, &allocator.int_tmp)) return true;
|
||||
|
||||
if (allocator.vec3_tmp.size() != allocator.int_tmp.size() || allocator.vec3_tmp2.size() != allocator.int_tmp.size()) return false;
|
||||
@@ -2745,7 +2765,7 @@ bool ShapeImpl::postprocess(GeometryImpl* geom, Allocator& allocator)
|
||||
normals = geom->normals;
|
||||
|
||||
Vec3* vr = &allocator.vec3_tmp[0];
|
||||
Vec3* nr = &allocator.vec3_tmp2[0];
|
||||
Vec3* nr = normals_element ? &allocator.vec3_tmp2[0] : nullptr;
|
||||
int* ir = &allocator.int_tmp[0];
|
||||
for (int i = 0, c = (int)allocator.int_tmp.size(); i < c; ++i)
|
||||
{
|
||||
@@ -2755,7 +2775,7 @@ bool ShapeImpl::postprocess(GeometryImpl* geom, Allocator& allocator)
|
||||
while (n)
|
||||
{
|
||||
vertices[n->index] = vertices[n->index] + vr[i];
|
||||
normals[n->index] = normals[n->index] + nr[i];
|
||||
if (normals_element) normals[n->index] = normals[n->index] + nr[i];
|
||||
n = n->next;
|
||||
}
|
||||
}
|
||||
@@ -2915,6 +2935,66 @@ static float getFramerateFromTimeMode(FrameRate time_mode, float custom_frame_ra
|
||||
}
|
||||
|
||||
|
||||
#define get_property(name, field, type, getter) if (node->first_property->value == name) \
|
||||
{ \
|
||||
IElementProperty* prop = node->getProperty(4); \
|
||||
if (prop) \
|
||||
{ \
|
||||
DataView value = prop->getValue(); \
|
||||
field = (type)value.getter(); \
|
||||
} \
|
||||
}
|
||||
#define get_time_property(name, field, type, getter) if (node->first_property->value == name) \
|
||||
{ \
|
||||
IElementProperty* prop = node->getProperty(4); \
|
||||
if (prop) \
|
||||
{ \
|
||||
DataView value = prop->getValue(); \
|
||||
field = fbxTimeToSeconds((type)value.getter()); \
|
||||
} \
|
||||
}
|
||||
#define get_text_property(name, field) if (node->first_property->value == name) \
|
||||
{ \
|
||||
IElementProperty* prop = node->getProperty(4); \
|
||||
if (prop) \
|
||||
{ \
|
||||
DataView value = prop->getValue(); \
|
||||
value.toString(field); \
|
||||
} \
|
||||
}
|
||||
|
||||
|
||||
static void parseGlobalInfo(const Element& root, Scene* scene)
|
||||
{
|
||||
for (Element* header = root.child; header; header = header->sibling)
|
||||
{
|
||||
if (header->id != "FBXHeaderExtension")
|
||||
continue;
|
||||
for (Element* info = header->child; info; info = info->sibling)
|
||||
{
|
||||
if (info->id != "SceneInfo")
|
||||
continue;
|
||||
for (Element* props70 = info->child; props70; props70 = props70->sibling)
|
||||
{
|
||||
if (props70->id != "Properties70")
|
||||
continue;
|
||||
for (Element* node = props70->child; node; node = node->sibling)
|
||||
{
|
||||
if (!node->first_property)
|
||||
continue;
|
||||
get_text_property("Original|ApplicationVendor", scene->m_info.AppVendor);
|
||||
get_text_property("Original|ApplicationName", scene->m_info.AppName);
|
||||
get_text_property("Original|ApplicationVersion", scene->m_info.AppVersion);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void parseGlobalSettings(const Element& root, Scene* scene)
|
||||
{
|
||||
for (Element* settings = root.child; settings; settings = settings->sibling)
|
||||
@@ -2929,44 +3009,20 @@ static void parseGlobalSettings(const Element& root, Scene* scene)
|
||||
{
|
||||
if (!node->first_property)
|
||||
continue;
|
||||
|
||||
#define get_property(name, field, type, getter) if(node->first_property->value == name) \
|
||||
{ \
|
||||
IElementProperty* prop = node->getProperty(4); \
|
||||
if (prop) \
|
||||
{ \
|
||||
DataView value = prop->getValue(); \
|
||||
scene->m_settings.field = (type)value.getter(); \
|
||||
} \
|
||||
}
|
||||
|
||||
#define get_time_property(name, field, type, getter) if(node->first_property->value == name) \
|
||||
{ \
|
||||
IElementProperty* prop = node->getProperty(4); \
|
||||
if (prop) \
|
||||
{ \
|
||||
DataView value = prop->getValue(); \
|
||||
scene->m_settings.field = fbxTimeToSeconds((type)value.getter()); \
|
||||
} \
|
||||
}
|
||||
|
||||
get_property("UpAxis", UpAxis, UpVector, toInt);
|
||||
get_property("UpAxisSign", UpAxisSign, int, toInt);
|
||||
get_property("FrontAxis", FrontAxis, FrontVector, toInt);
|
||||
get_property("FrontAxisSign", FrontAxisSign, int, toInt);
|
||||
get_property("CoordAxis", CoordAxis, CoordSystem, toInt);
|
||||
get_property("CoordAxisSign", CoordAxisSign, int, toInt);
|
||||
get_property("OriginalUpAxis", OriginalUpAxis, int, toInt);
|
||||
get_property("OriginalUpAxisSign", OriginalUpAxisSign, int, toInt);
|
||||
get_property("UnitScaleFactor", UnitScaleFactor, float, toDouble);
|
||||
get_property("OriginalUnitScaleFactor", OriginalUnitScaleFactor, float, toDouble);
|
||||
get_time_property("TimeSpanStart", TimeSpanStart, u64, toU64);
|
||||
get_time_property("TimeSpanStop", TimeSpanStop, u64, toU64);
|
||||
get_property("TimeMode", TimeMode, FrameRate, toInt);
|
||||
get_property("CustomFrameRate", CustomFrameRate, float, toDouble);
|
||||
|
||||
#undef get_property
|
||||
|
||||
get_property("UpAxis", scene->m_settings.UpAxis, UpVector, toInt);
|
||||
get_property("UpAxisSign", scene->m_settings.UpAxisSign, int, toInt);
|
||||
get_property("FrontAxis", scene->m_settings.FrontAxis, FrontVector, toInt);
|
||||
get_property("FrontAxisSign", scene->m_settings.FrontAxisSign, int, toInt);
|
||||
get_property("CoordAxis", scene->m_settings.CoordAxis, CoordSystem, toInt);
|
||||
get_property("CoordAxisSign", scene->m_settings.CoordAxisSign, int, toInt);
|
||||
get_property("OriginalUpAxis", scene->m_settings.OriginalUpAxis, int, toInt);
|
||||
get_property("OriginalUpAxisSign", scene->m_settings.OriginalUpAxisSign, int, toInt);
|
||||
get_property("UnitScaleFactor", scene->m_settings.UnitScaleFactor, float, toDouble);
|
||||
get_property("OriginalUnitScaleFactor", scene->m_settings.OriginalUnitScaleFactor, float, toDouble);
|
||||
get_time_property("TimeSpanStart", scene->m_settings.TimeSpanStart, u64, toU64);
|
||||
get_time_property("TimeSpanStop", scene->m_settings.TimeSpanStop, u64, toU64);
|
||||
get_property("TimeMode", scene->m_settings.TimeMode, FrameRate, toInt);
|
||||
get_property("CustomFrameRate", scene->m_settings.CustomFrameRate, float, toDouble);
|
||||
scene->m_scene_frame_rate = getFramerateFromTimeMode(scene->m_settings.TimeMode, scene->m_settings.CustomFrameRate);
|
||||
}
|
||||
break;
|
||||
@@ -2978,6 +3034,10 @@ static void parseGlobalSettings(const Element& root, Scene* scene)
|
||||
}
|
||||
|
||||
|
||||
#undef get_property
|
||||
#undef get_time_property
|
||||
|
||||
|
||||
struct ParseGeometryJob {
|
||||
const Element* element;
|
||||
bool triangulate;
|
||||
@@ -3520,7 +3580,7 @@ Object* Object::resolveObjectLink(int idx) const
|
||||
if (connection.to == id && connection.from != 0)
|
||||
{
|
||||
Object* obj = scene.m_object_map.find(connection.from)->second.object;
|
||||
if (obj)
|
||||
if (obj && obj->is_node && obj != this && connection.type == Scene::Connection::OBJECT_OBJECT)
|
||||
{
|
||||
if (idx == 0) return obj;
|
||||
--idx;
|
||||
@@ -3561,7 +3621,7 @@ Object* Object::getParent() const
|
||||
if (connection.from == id)
|
||||
{
|
||||
Object* obj = scene.m_object_map.find(connection.to)->second.object;
|
||||
if (obj && obj->is_node)
|
||||
if (obj && obj->is_node && obj != this && connection.type == Scene::Connection::OBJECT_OBJECT)
|
||||
{
|
||||
assert(parent == nullptr);
|
||||
parent = obj;
|
||||
@@ -3606,6 +3666,7 @@ IScene* load(const u8* data, int size, u64 flags, JobProcessor job_processor, vo
|
||||
if (!parseConnections(*root.getValue(), scene.get())) return nullptr;
|
||||
if (!parseTakes(scene.get())) return nullptr;
|
||||
if (!parseObjects(*root.getValue(), scene.get(), flags, scene->m_allocator, job_processor, job_user_ptr)) return nullptr;
|
||||
parseGlobalInfo(*root.getValue(), scene.get());
|
||||
parseGlobalSettings(*root.getValue(), scene.get());
|
||||
|
||||
return scene.release();
|
||||
|
||||
9
Source/ThirdParty/OpenFBX/ofbx.h
vendored
9
Source/ThirdParty/OpenFBX/ofbx.h
vendored
@@ -511,6 +511,14 @@ struct GlobalSettings
|
||||
};
|
||||
|
||||
|
||||
struct GlobalInfo
|
||||
{
|
||||
char AppVendor[128];
|
||||
char AppName[128];
|
||||
char AppVersion[128];
|
||||
};
|
||||
|
||||
|
||||
struct IScene
|
||||
{
|
||||
virtual void destroy() = 0;
|
||||
@@ -519,6 +527,7 @@ struct IScene
|
||||
virtual const TakeInfo* getTakeInfo(const char* name) const = 0;
|
||||
virtual int getMeshCount() const = 0;
|
||||
virtual float getSceneFrameRate() const = 0;
|
||||
virtual const GlobalInfo* getGlobalInfo() const = 0;
|
||||
virtual const GlobalSettings* getGlobalSettings() const = 0;
|
||||
virtual const Mesh* getMesh(int index) const = 0;
|
||||
virtual int getAnimationStackCount() const = 0;
|
||||
|
||||
21
Source/ThirdParty/pugixml/pugixml.cpp
vendored
21
Source/ThirdParty/pugixml/pugixml.cpp
vendored
@@ -4799,16 +4799,6 @@ namespace pugi
|
||||
return xml_node();
|
||||
}
|
||||
|
||||
PUGI__FN xml_node xml_node::child_or_append(const char_t* name_)
|
||||
{
|
||||
if (!_root) return xml_node();
|
||||
|
||||
for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
|
||||
if (i->name && impl::strequal(name_, i->name)) return xml_node(i);
|
||||
|
||||
return append_child(name_);
|
||||
}
|
||||
|
||||
PUGI__FN xml_attribute xml_node::attribute(const char_t* name_) const
|
||||
{
|
||||
if (!_root) return xml_attribute();
|
||||
@@ -4879,17 +4869,6 @@ namespace pugi
|
||||
return PUGIXML_TEXT("");
|
||||
}
|
||||
|
||||
PUGI__FN bool xml_node::set_child_value(const char_t* rhs)
|
||||
{
|
||||
if (!_root) return false;
|
||||
|
||||
for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
|
||||
if (i->value && impl::is_text_node(i))
|
||||
return xml_node(i).set_value(rhs);
|
||||
|
||||
return append_child(node_pcdata).set_value(rhs);
|
||||
}
|
||||
|
||||
PUGI__FN const char_t* xml_node::child_value(const char_t* name_) const
|
||||
{
|
||||
return child(name_).child_value();
|
||||
|
||||
2
Source/ThirdParty/pugixml/pugixml.hpp
vendored
2
Source/ThirdParty/pugixml/pugixml.hpp
vendored
@@ -452,14 +452,12 @@ namespace pugi
|
||||
|
||||
// Get child, attribute or next/previous sibling with the specified name
|
||||
xml_node child(const char_t* name) const;
|
||||
xml_node child_or_append(const char_t* name);
|
||||
xml_attribute attribute(const char_t* name) const;
|
||||
xml_node next_sibling(const char_t* name) const;
|
||||
xml_node previous_sibling(const char_t* name) const;
|
||||
|
||||
// Get child value of current node; that is, value of the first child node of type PCDATA/CDATA
|
||||
const char_t* child_value() const;
|
||||
bool set_child_value(const char_t* rhs);
|
||||
|
||||
// Get child value of child with specified name. Equivalent to child(name).child_value().
|
||||
const char_t* child_value(const char_t* name) const;
|
||||
|
||||
63
Source/ThirdParty/pugixml/pugixml_extra.cpp
vendored
Normal file
63
Source/ThirdParty/pugixml/pugixml_extra.cpp
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "pugixml.hpp"
|
||||
#include "pugixml_extra.hpp"
|
||||
|
||||
namespace pugi
|
||||
{
|
||||
|
||||
// Compare two strings
|
||||
// This comes from pugi::impl::strequal
|
||||
bool strequal(const char_t* src, const char_t* dst)
|
||||
{
|
||||
#ifdef PUGIXML_WCHAR_MODE
|
||||
return wcscmp(src, dst) == 0;
|
||||
#else
|
||||
return strcmp(src, dst) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// This comes from pugi::impl::is_text_node
|
||||
inline bool is_text_node(const xml_node& node)
|
||||
{
|
||||
xml_node_type type = node.type();
|
||||
|
||||
return type == node_pcdata || type == node_cdata;
|
||||
}
|
||||
|
||||
xml_node_extra::xml_node_extra(xml_node child) : xml_node(child)
|
||||
{
|
||||
}
|
||||
|
||||
xml_node_extra xml_node_extra::child_or_append(const char_t* name_)
|
||||
{
|
||||
if (!name_ || !_root) return xml_node_extra();
|
||||
|
||||
for (xml_node& child : *this)
|
||||
{
|
||||
const auto *name = child.name();
|
||||
if (name && strequal(name_, name)) return xml_node_extra(child);
|
||||
}
|
||||
|
||||
return xml_node_extra(append_child(name_));
|
||||
}
|
||||
|
||||
bool xml_node_extra::set_child_value(const char_t* rhs)
|
||||
{
|
||||
if (!_root) return xml_node();
|
||||
|
||||
for (xml_node& child : *this)
|
||||
{
|
||||
if (child.value() && is_text_node(child))
|
||||
{
|
||||
return child.set_value(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
return append_child(node_pcdata).set_value(rhs);
|
||||
}
|
||||
|
||||
void xml_node_extra::append_child_with_value(const char_t* name_, const char_t* rhs)
|
||||
{
|
||||
xml_node_extra child = xml_node_extra(append_child(name_));
|
||||
child.append_child(node_pcdata).set_value(rhs);
|
||||
}
|
||||
}
|
||||
18
Source/ThirdParty/pugixml/pugixml_extra.hpp
vendored
Normal file
18
Source/ThirdParty/pugixml/pugixml_extra.hpp
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "pugixml.hpp"
|
||||
|
||||
namespace pugi
|
||||
{
|
||||
class xml_node_extra : public xml_node
|
||||
{
|
||||
public:
|
||||
xml_node_extra() = default;
|
||||
xml_node_extra(xml_node child);
|
||||
|
||||
xml_node_extra child_or_append(const char_t* name_);
|
||||
bool set_child_value(const char_t* rhs);
|
||||
void append_child_with_value(const char_t* name_, const char_t* rhs);
|
||||
|
||||
};
|
||||
}
|
||||
@@ -244,7 +244,19 @@ namespace Flax.Build
|
||||
/// <returns>The toolchain.</returns>
|
||||
public Toolchain TryGetToolchain(TargetArchitecture targetArchitecture)
|
||||
{
|
||||
return HasRequiredSDKsInstalled ? GetToolchain(targetArchitecture) : null;
|
||||
Toolchain result = null;
|
||||
if (HasRequiredSDKsInstalled)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = GetToolchain(targetArchitecture);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Exception(ex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -260,16 +272,12 @@ namespace Flax.Build
|
||||
if (_toolchains == null)
|
||||
_toolchains = new Dictionary<TargetArchitecture, Toolchain>();
|
||||
|
||||
var key = targetArchitecture;
|
||||
|
||||
Toolchain toolchain;
|
||||
if (_toolchains.TryGetValue(key, out toolchain))
|
||||
{
|
||||
if (_toolchains.TryGetValue(targetArchitecture, out toolchain))
|
||||
return toolchain;
|
||||
}
|
||||
|
||||
toolchain = CreateToolchain(targetArchitecture);
|
||||
_toolchains.Add(key, toolchain);
|
||||
_toolchains.Add(targetArchitecture, toolchain);
|
||||
|
||||
return toolchain;
|
||||
}
|
||||
|
||||
@@ -39,12 +39,6 @@ namespace Flax.Build
|
||||
[CommandLine("deploy", "Runs the deploy tool.")]
|
||||
public static bool Deploy = false;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses deployed files.
|
||||
/// </summary>
|
||||
[CommandLine("deployDontCompress", "Skips compressing deployed files, and keeps files.")]
|
||||
public static bool DontCompress = false;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the targets. Builds all the targets, use <see cref="BuildTargets"/> to select a custom set of targets for the build.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace Flax.Build
|
||||
{
|
||||
public static partial class Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Compresses deployed files.
|
||||
/// </summary>
|
||||
[CommandLine("deployDontCompress", "Skips compressing deployed files, and keeps files.")]
|
||||
public static bool DontCompress = false;
|
||||
|
||||
/// <summary>
|
||||
/// Package deployment output path.
|
||||
/// </summary>
|
||||
@@ -28,9 +34,9 @@ namespace Flax.Build
|
||||
public static bool DeployPlatforms;
|
||||
|
||||
/// <summary>
|
||||
/// Certificate file path for binaries signing.
|
||||
/// Certificate file path for binaries signing. Or sign identity for Apple platforms.
|
||||
/// </summary>
|
||||
[CommandLine("deployCert", "Certificate file path for binaries signing.")]
|
||||
[CommandLine("deployCert", "Certificate file path for binaries signing. Or sign identity for Apple platforms.")]
|
||||
public static string DeployCert;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Flax.Build;
|
||||
using Flax.Build.Platforms;
|
||||
|
||||
@@ -17,11 +18,15 @@ namespace Flax.Deploy
|
||||
{
|
||||
if (string.IsNullOrEmpty(Configuration.DeployCert))
|
||||
return;
|
||||
Log.Info("Code signing file: " + file);
|
||||
switch (Platform.BuildTargetPlatform)
|
||||
{
|
||||
case TargetPlatform.Windows:
|
||||
VCEnvironment.CodeSign(file, Configuration.DeployCert, Configuration.DeployCertPass);
|
||||
break;
|
||||
case TargetPlatform.Mac:
|
||||
MacPlatform.CodeSign(file, Configuration.DeployCert);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +127,45 @@ namespace Flax.Deploy
|
||||
// Deploy project
|
||||
DeployFile(RootPath, OutputPath, "Flax.flaxproj");
|
||||
|
||||
// Package Editor into macOS app
|
||||
if (Platform.BuildTargetPlatform == TargetPlatform.Mac)
|
||||
{
|
||||
var arch = Platform.BuildTargetArchitecture;
|
||||
Log.Info(string.Empty);
|
||||
Log.Info("Creating macOS app...");
|
||||
var appPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.app");
|
||||
var appContentsPath = Path.Combine(appPath, "Contents");
|
||||
var appBinariesPath = Path.Combine(appContentsPath, "MacOS");
|
||||
Utilities.DirectoryDelete(appPath);
|
||||
Directory.CreateDirectory(appPath);
|
||||
Directory.CreateDirectory(appContentsPath);
|
||||
Directory.CreateDirectory(appBinariesPath);
|
||||
|
||||
// Copy icons set
|
||||
Directory.CreateDirectory(Path.Combine(appContentsPath, "Resources"));
|
||||
Utilities.FileCopy(Path.Combine(Globals.EngineRoot, "Source/Platforms/Mac/Default.icns"), Path.Combine(appContentsPath, "Resources/icon.icns"));
|
||||
|
||||
// Create PkgInfo file
|
||||
File.WriteAllText(Path.Combine(appContentsPath, "PkgInfo"), "APPL???", Encoding.ASCII);
|
||||
|
||||
// Create Info.plist
|
||||
var plist = File.ReadAllText(Path.Combine(Globals.EngineRoot, "Source/Platforms/Mac/Default.plist"));
|
||||
var flaxProject = EngineTarget.EngineProject;
|
||||
plist = plist.Replace("{Version}", flaxProject.Version.ToString());
|
||||
plist = plist.Replace("{Copyright}", flaxProject.Copyright);
|
||||
plist = plist.Replace("{Executable}", "FlaxEditor");
|
||||
plist = plist.Replace("{Arch}", arch == TargetArchitecture.ARM64 ? "arm64" : "x86_64");
|
||||
File.WriteAllText(Path.Combine(appContentsPath, "Info.plist"), plist, Encoding.ASCII);
|
||||
|
||||
// Copy output editor files
|
||||
Utilities.DirectoryCopy(OutputPath, appContentsPath);
|
||||
|
||||
// Copy native binaries for app execution
|
||||
var defaultEditorConfig = "Development";
|
||||
var ediotrBinariesPath = Path.Combine(appContentsPath, "Binaries/Editor/Mac", defaultEditorConfig);
|
||||
Utilities.DirectoryCopy(ediotrBinariesPath, appBinariesPath, true, true);
|
||||
}
|
||||
|
||||
// Compress
|
||||
if (Configuration.DontCompress)
|
||||
return;
|
||||
@@ -254,6 +298,10 @@ namespace Flax.Deploy
|
||||
Utilities.Run("strip", "FlaxEditor", null, dst, Utilities.RunOptions.None);
|
||||
Utilities.Run("strip", "FlaxEditor.dylib", null, dst, Utilities.RunOptions.None);
|
||||
Utilities.Run("strip", "libMoltenVK.dylib", null, dst, Utilities.RunOptions.None);
|
||||
|
||||
CodeSign(Path.Combine(dst, "FlaxEditor"));
|
||||
CodeSign(Path.Combine(dst, "FlaxEditor.dylib"));
|
||||
CodeSign(Path.Combine(dst, "libMoltenVK.dylib"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,19 +42,7 @@ namespace Flax.Build.Platforms
|
||||
var subdirs = Directory.GetDirectories(Path.Combine(AndroidSdk.Instance.RootPath, "ndk"));
|
||||
if (subdirs.Length != 0)
|
||||
{
|
||||
Array.Sort(subdirs, (a, b) =>
|
||||
{
|
||||
Version va, vb;
|
||||
if (Version.TryParse(a, out va))
|
||||
{
|
||||
if (Version.TryParse(b, out vb))
|
||||
return va.CompareTo(vb);
|
||||
return 1;
|
||||
}
|
||||
if (Version.TryParse(b, out vb))
|
||||
return -1;
|
||||
return 0;
|
||||
});
|
||||
Utilities.SortVersionDirectories(subdirs);
|
||||
sdkPath = subdirs.Last();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Flax.Build.Platforms
|
||||
public LinuxPlatform()
|
||||
{
|
||||
// Try to use system compiler
|
||||
if (Platform.BuildTargetPlatform == TargetPlatform.Linux)
|
||||
if (BuildTargetPlatform == TargetPlatform.Linux)
|
||||
{
|
||||
// Pick the newest compiler (overriden by specified in command line)
|
||||
if (Which(Configuration.Compiler) != null)
|
||||
@@ -67,7 +67,7 @@ namespace Flax.Build.Platforms
|
||||
Log.Verbose($"Using native Linux toolchain (compiler {Compiler})");
|
||||
HasRequiredSDKsInstalled = true;
|
||||
}
|
||||
else if (Platform.BuildTargetPlatform != TargetPlatform.Mac)
|
||||
else if (BuildTargetPlatform != TargetPlatform.Mac)
|
||||
{
|
||||
// Check if Linux toolchain is installed
|
||||
string toolchainName = "v13_clang-7.0.1-centos7";
|
||||
@@ -76,9 +76,11 @@ namespace Flax.Build.Platforms
|
||||
{
|
||||
if (string.IsNullOrEmpty(toolchainsRoot))
|
||||
{
|
||||
Log.Warning("Missing Linux Toolchain. Cannot build for Linux platform.");
|
||||
if (BuildTargetPlatform == TargetPlatform.Linux)
|
||||
Log.Warning("Missing Linux Toolchain. Cannot build for Linux platform.");
|
||||
else
|
||||
Log.Verbose("Missing Linux Toolchain. Cannot build for Linux platform.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Flax.Build.Platforms
|
||||
@@ -44,6 +46,24 @@ namespace Flax.Build.Platforms
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs codesign tool on macOS to sign the code with a given identity from local keychain.
|
||||
/// </summary>
|
||||
/// <param name="file">Path to file to codesign.</param>
|
||||
/// <param name="signIdenity">App code signing idenity name (from local Mac keychain). Use 'security find-identity -v -p codesigning' to list possible options.</param>
|
||||
public static void CodeSign(string file, string signIdenity)
|
||||
{
|
||||
if (!File.Exists(file))
|
||||
throw new FileNotFoundException("Missing file to sign.", file);
|
||||
string cmdLine = string.Format("--force --timestamp -s \"{0}\" \"{1}\"", signIdenity, file);
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(file)))
|
||||
{
|
||||
// Add entitlements file with some settings for the app execution
|
||||
cmdLine += string.Format(" --entitlements \"{0}\"", Path.Combine(Globals.EngineRoot, "Source/Platforms/Mac/Default.entitlements"));
|
||||
}
|
||||
Utilities.Run("codesign", cmdLine, null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if running an x64 binary an arm64 host machine.
|
||||
/// </summary>
|
||||
|
||||
@@ -24,6 +24,13 @@ namespace Flax.Build.Projects.VisualStudio
|
||||
/// <inheritdoc />
|
||||
public override void Generate(string solutionPath)
|
||||
{
|
||||
// Try to reuse the existing project guid from existing files
|
||||
ProjectGuid = GetProjectGuid(Path, Name);
|
||||
if (ProjectGuid == Guid.Empty)
|
||||
ProjectGuid = GetProjectGuid(solutionPath, Name);
|
||||
if (ProjectGuid == Guid.Empty)
|
||||
ProjectGuid = Guid.NewGuid();
|
||||
|
||||
var gen = (VisualStudioProjectGenerator)Generator;
|
||||
var projectFileToolVersion = gen.ProjectFileToolVersion;
|
||||
var vcProjectFileContent = new StringBuilder();
|
||||
@@ -500,7 +507,15 @@ namespace Flax.Build.Projects.VisualStudio
|
||||
firstEditorMatch = i;
|
||||
}
|
||||
}
|
||||
if (firstFullMatch != -1)
|
||||
if (project is AndroidProject)
|
||||
{
|
||||
// Utility Android deploy project only for exact match
|
||||
if (firstFullMatch != -1)
|
||||
projectConfiguration = configuration;
|
||||
else
|
||||
projectConfiguration = new SolutionConfiguration(project.Configurations[0]);
|
||||
}
|
||||
else if (firstFullMatch != -1)
|
||||
{
|
||||
projectConfiguration = configuration;
|
||||
build = solution.MainProject == project || (solution.MainProject == null && project.Name == solution.Name);
|
||||
@@ -588,8 +603,8 @@ namespace Flax.Build.Projects.VisualStudio
|
||||
{
|
||||
var profiles = new Dictionary<string, string>();
|
||||
var profile = new StringBuilder();
|
||||
var editorPath = Utilities.NormalizePath(Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), $"Development/FlaxEditor{Utilities.GetPlatformExecutableExt()}"));
|
||||
var workspacePath = Utilities.NormalizePath(solutionDirectory);
|
||||
var editorPath = Utilities.NormalizePath(Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), $"Development/FlaxEditor{Utilities.GetPlatformExecutableExt()}")).Replace('\\', '/');
|
||||
var workspacePath = Utilities.NormalizePath(solutionDirectory).Replace('\\', '/');
|
||||
foreach (var project in projects)
|
||||
{
|
||||
if (project.Type == TargetType.DotNetCore)
|
||||
|
||||
@@ -758,8 +758,30 @@ namespace Flax.Build
|
||||
{
|
||||
extEnding = "";
|
||||
}
|
||||
|
||||
return extEnding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the directories by name assuming they contain version text. Sorted from lowest to the highest version.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths array to sort.</param>
|
||||
public static void SortVersionDirectories(string[] paths)
|
||||
{
|
||||
if (paths == null || paths.Length == 0)
|
||||
return;
|
||||
Array.Sort(paths, (a, b) =>
|
||||
{
|
||||
Version va, vb;
|
||||
if (Version.TryParse(a, out va))
|
||||
{
|
||||
if (Version.TryParse(b, out vb))
|
||||
return va.CompareTo(vb);
|
||||
return 1;
|
||||
}
|
||||
if (Version.TryParse(b, out vb))
|
||||
return -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user